Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ namespace Masa.Auth.Contracts.Admin.Infrastructure.Password;
/// A client-level rule (stored on ClientConfig) takes precedence; when absent it falls back to
/// the global DCC configuration.
/// </summary>
public interface IPasswordRuleProvider
public interface IPasswordRuleProvider : IScopedDependency
{
/// <summary>
/// Validate a password against the client's effective rule.
/// Returns the localized failure prompt, or null when the password is valid.
/// Designed to be called from a FluentValidation rule (see <see cref="PasswordRuleValidatorExtensions"/>)
/// so client-aware password validation stays inside the single, framework-invoked validation pipeline.
/// </summary>
string? GetFailure(string? password, string? clientId);

Task<string?> GetFailureAsync(string? password, string? clientId);

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ public static class PasswordRuleValidatorExtensions
IPasswordRuleProvider passwordRuleProvider,
Func<T, string?> clientId)
{
return ruleBuilder.CustomAsync(async (password, context, cancellation) =>
return ruleBuilder.Custom((password, context) =>
{
var failure = await passwordRuleProvider.GetFailureAsync(password, clientId(context.InstanceToValidate));
var failure = passwordRuleProvider.GetFailure(password, clientId(context.InstanceToValidate));
if (failure is not null)
{
context.AddFailure(failure);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ public ResetPasswordCommandValidator(PhoneNumberValidator phoneValidator, IPassw
RuleFor(x => x.Voucher).Required().Email();
});
RuleFor(x => x.Captcha).Required();
// Anonymous forgot-password flow (no login) - the client rule follows whatever ClientId
// the caller passes for this request; falls back to the global DCC rule when absent.
RuleFor(x => x.Password).PasswordRule(passwordRuleProvider, x => x.ClientId);
RuleFor(x => x.ConfirmPassword).Equal(x => x.Password);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Masa.Auth.Service.Admin.Infrastructure.Message;

public class ClientMessageTemplateProvider : IClientMessageTemplateProvider, IScopedDependency
public class ClientMessageTemplateProvider : IClientMessageTemplateProvider
{
private readonly IClientConfigRepository _clientConfigRepository;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Masa.Auth.Service.Admin.Infrastructure.Message;
/// (channel type, scene). Returns null when the client has no matching configuration,
/// signalling the caller to fall back to the global DCC SmsOptions/EmailOptions.
/// </summary>
public interface IClientMessageTemplateProvider
public interface IClientMessageTemplateProvider : IScopedDependency
{
Task<(string channelCode, string templateCode)?> ResolveAsync(string? clientId, ChannelTypes channelType, int scene);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Masa.Auth.Service.Admin.Infrastructure.Password;

public class PasswordRuleProvider : IPasswordRuleProvider, IScopedDependency
public class PasswordRuleProvider : IPasswordRuleProvider
{
public const string PASSWORDRULECONFIGNAME = "$public.AppSettings:PasswordRule";
public const string PASSWORDPROMPTCONFIGNAME = "$public.AppSettings:PasswordPrompt";
Expand All @@ -26,6 +26,12 @@ public PasswordRuleProvider(
_logger = logger;
}

public string? GetFailure(string? password, string? clientId)
{
var (rule, prompt) = GetEffectiveRule(clientId);
return ValidatePasswordWithRule(password, rule, prompt);
}

public async Task<string?> GetFailureAsync(string? password, string? clientId)
{
var (rule, prompt) = await GetEffectiveRuleAsync(clientId);
Expand All @@ -38,6 +44,26 @@ public Task<string> GenerateNewPasswordAsync()
return Task.FromResult(GenerateNewPassword(passwordRule));
}

private (string rule, string prompt) GetEffectiveRule(string? clientId)
{
var (rule, prompt) = GetGlobalPasswordRule();

if (!string.IsNullOrEmpty(clientId))
{
var clientConfig = _clientConfigRepository.FindByClientIdAsync(clientId).Result;
if (clientConfig is not null && !string.IsNullOrEmpty(clientConfig.PasswordRule))
{
rule = clientConfig.PasswordRule;
if (!string.IsNullOrEmpty(clientConfig.PasswordPrompt))
{
prompt = clientConfig.PasswordPrompt;
}
}
}

return (rule, prompt);
}

private async Task<(string rule, string prompt)> GetEffectiveRuleAsync(string? clientId)
{
// 全局 DCC 配置作为回退默认值
Expand Down
1 change: 0 additions & 1 deletion src/Services/Masa.Auth.Service.Admin/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
#endif

builder.Services.AddAutoInject();
builder.Services.AddScoped<IClientMessageTemplateProvider, ClientMessageTemplateProvider>();
builder.Services.AddDaprClient();

builder.Services.AddObjectStorage(option => option.UseAliyunStorage());
Expand Down
1 change: 1 addition & 0 deletions src/Services/Masa.Auth.Service.Admin/_Imports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
global using Masa.Auth.Service.Admin.Infrastructure.Message;
global using Masa.Auth.Service.Admin.Infrastructure.Middleware;
global using Masa.Auth.Service.Admin.Infrastructure.Models;
global using Masa.Auth.Service.Admin.Infrastructure.Password;
global using Masa.Auth.Service.Admin.Infrastructure.Sms;
global using Masa.Auth.Service.Admin.Infrastructure.Utils;
global using Masa.BuildingBlocks.Authentication.Identity;
Expand Down
Loading