forked from gongxuegit/tiku-backend.net
feat: harden SaaS authentication and authorization
This commit is contained in:
@@ -4,6 +4,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Growth;
|
||||
@@ -13,7 +14,9 @@ using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Growth;
|
||||
|
||||
public sealed class CommissionService(TikuDbContext dbContext) : ICommissionService
|
||||
public sealed class CommissionService(
|
||||
TikuDbContext dbContext,
|
||||
ICurrentAccessContext currentAccessContext) : ICommissionService
|
||||
{
|
||||
public async Task<CommissionSettingsItem> GetSettingsAsync(CommissionAdminActor actor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -40,11 +43,20 @@ public sealed class CommissionService(TikuDbContext dbContext) : ICommissionServ
|
||||
var member = await dbContext.TenantMemberships
|
||||
.FirstOrDefaultAsync(item => item.TenantId == actor.TenantId && item.UserId == command.UserId, cancellationToken)
|
||||
?? throw new CommissionException("Commission member was not found.", "commission_member_not_found");
|
||||
member.Permissions = JsonSerializer.SerializeToElement(new
|
||||
var settings = await GetSettingsCoreAsync(actor.TenantId, cancellationToken);
|
||||
var config = settings.Config.ValueKind == JsonValueKind.Object
|
||||
? JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(settings.Config.GetRawText()) ?? []
|
||||
: [];
|
||||
var memberRates = config.TryGetValue("memberRates", out var existingRates) && existingRates.ValueKind == JsonValueKind.Object
|
||||
? JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(existingRates.GetRawText()) ?? []
|
||||
: [];
|
||||
memberRates[member.UserId.ToString("N")] = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
commissionRate = command.CommissionRate,
|
||||
commissionConfig = command.CommissionConfig
|
||||
});
|
||||
config["memberRates"] = JsonSerializer.SerializeToElement(memberRates);
|
||||
settings.Config = JsonSerializer.SerializeToElement(config);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new { member.UserId, commissionRate = command.CommissionRate, commissionConfig = command.CommissionConfig };
|
||||
}
|
||||
@@ -265,7 +277,7 @@ public sealed class CommissionService(TikuDbContext dbContext) : ICommissionServ
|
||||
foreach (var row in orders)
|
||||
{
|
||||
if (query.ReferrerUserId.HasValue && row.lead.ReferrerUserId != query.ReferrerUserId) continue;
|
||||
var rate = GetMemberRate(await GetMembershipPermissionsAsync(tenantId, row.lead.ReferrerUserId!.Value, cancellationToken)) ?? settings.DefaultRate;
|
||||
var rate = GetMemberRate(settings.Config, row.lead.ReferrerUserId!.Value) ?? settings.DefaultRate;
|
||||
var settled = existing.FirstOrDefault(item => item.SourceType == CommissionSourceType.Order && item.SourceId == row.order.Id)?.SettlementId;
|
||||
result.Add(new SourceCandidate(CommissionSourceType.Order, row.order.Id, row.order.OrderNo, row.lead.ReferrerUserId.Value, row.order.UserId, row.order.AmountCents, rate, (int)Math.Round(row.order.AmountCents * rate), CommissionRateSource.Member, settled, row.order.PaidAt, "protected_lead"));
|
||||
}
|
||||
@@ -282,7 +294,7 @@ public sealed class CommissionService(TikuDbContext dbContext) : ICommissionServ
|
||||
if (!row.code.AgentUserId.HasValue) continue;
|
||||
var agentUserId = row.code.AgentUserId.Value;
|
||||
if (query.ReferrerUserId.HasValue && agentUserId != query.ReferrerUserId) continue;
|
||||
var rate = row.batch?.CommissionRate ?? GetMemberRate(await GetMembershipPermissionsAsync(tenantId, agentUserId, cancellationToken)) ?? settings.DefaultRate;
|
||||
var rate = row.batch?.CommissionRate ?? GetMemberRate(settings.Config, agentUserId) ?? settings.DefaultRate;
|
||||
var sourceAmount = row.code.UnitPriceCents ?? row.batch?.DefaultUnitPriceCents ?? 0;
|
||||
var settled = existing.FirstOrDefault(item => item.SourceType == CommissionSourceType.ActivationCode && item.SourceId == row.code.Id)?.SettlementId;
|
||||
result.Add(new SourceCandidate(CommissionSourceType.ActivationCode, row.code.Id, row.code.Code, agentUserId, row.code.UsedBy, sourceAmount, rate, (int)Math.Round(sourceAmount * rate), row.batch?.CommissionRate is null ? CommissionRateSource.Member : CommissionRateSource.Batch, settled, row.code.UsedAt, "activation_code_agent"));
|
||||
@@ -290,11 +302,22 @@ public sealed class CommissionService(TikuDbContext dbContext) : ICommissionServ
|
||||
return result.OrderBy(item => item.SourcePaidAt).ToArray();
|
||||
}
|
||||
|
||||
private async Task<JsonElement> GetMembershipPermissionsAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken) =>
|
||||
await dbContext.TenantMemberships.AsNoTracking().Where(item => item.TenantId == tenantId && item.UserId == userId).Select(item => item.Permissions).FirstOrDefaultAsync(cancellationToken);
|
||||
private static decimal? GetMemberRate(JsonElement config, Guid userId)
|
||||
{
|
||||
if (config.ValueKind != JsonValueKind.Object ||
|
||||
!config.TryGetProperty("memberRates", out var memberRates) ||
|
||||
memberRates.ValueKind != JsonValueKind.Object ||
|
||||
!memberRates.TryGetProperty(userId.ToString("N"), out var memberRate) ||
|
||||
memberRate.ValueKind != JsonValueKind.Object ||
|
||||
!memberRate.TryGetProperty("commissionRate", out var value) ||
|
||||
value.ValueKind != JsonValueKind.Number ||
|
||||
!value.TryGetDecimal(out var rate))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private static decimal? GetMemberRate(JsonElement permissions) =>
|
||||
permissions.ValueKind == JsonValueKind.Object && permissions.TryGetProperty("commissionRate", out var value) && value.ValueKind == JsonValueKind.Number && value.TryGetDecimal(out var rate) ? rate : null;
|
||||
return rate;
|
||||
}
|
||||
|
||||
private async Task<TenantCommissionSetting> GetSettingsCoreAsync(Guid tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -311,8 +334,14 @@ public sealed class CommissionService(TikuDbContext dbContext) : ICommissionServ
|
||||
|
||||
private async Task AssertAdminAsync(CommissionAdminActor actor, CancellationToken cancellationToken)
|
||||
{
|
||||
var ok = await dbContext.TenantMemberships.AnyAsync(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && item.Status == MembershipStatus.Active && (item.Role == TenantRole.PlatformAdmin || item.Role == TenantRole.TenantOwner || item.Role == TenantRole.TenantAdmin), cancellationToken);
|
||||
if (!ok) throw new CommissionException("Commission admin access was denied.", "commission_access_denied");
|
||||
var access = await currentAccessContext.GetAsync(cancellationToken);
|
||||
if (!access.IsCurrentTenantMember ||
|
||||
access.TenantId != actor.TenantId ||
|
||||
access.UserId != actor.UserId ||
|
||||
!access.HasTenantPermission(BackendPermissions.TenantCommissionManage))
|
||||
{
|
||||
throw new CommissionException("Commission admin access was denied.", "commission_access_denied");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddAuditAsync(CommissionAdminActor actor, string action, string targetType, Guid targetId, object details, CancellationToken cancellationToken)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Growth;
|
||||
using Tiku.Domain.Tenancy;
|
||||
@@ -11,7 +12,8 @@ namespace Tiku.Infrastructure.Growth;
|
||||
|
||||
internal sealed class CrmService(
|
||||
TikuDbContext dbContext,
|
||||
ITenantSecretProtector tenantSecretProtector) : ICrmService
|
||||
ITenantSecretProtector tenantSecretProtector,
|
||||
ICurrentAccessContext currentAccessContext) : ICrmService
|
||||
{
|
||||
private static readonly HashSet<string> SensitiveKeys = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -270,16 +272,12 @@ internal sealed class CrmService(
|
||||
|
||||
private async Task AssertAdminAsync(CrmAdminActor actor, CancellationToken cancellationToken)
|
||||
{
|
||||
var isAdmin = await dbContext.TenantMemberships.AnyAsync(
|
||||
item =>
|
||||
item.TenantId == actor.TenantId &&
|
||||
item.UserId == actor.UserId &&
|
||||
item.Status == MembershipStatus.Active &&
|
||||
(item.Role == TenantRole.PlatformAdmin ||
|
||||
item.Role == TenantRole.TenantOwner ||
|
||||
item.Role == TenantRole.TenantAdmin),
|
||||
cancellationToken);
|
||||
if (!isAdmin)
|
||||
var access = await currentAccessContext.GetAsync(cancellationToken);
|
||||
if (!access.IsCurrentTenantMember ||
|
||||
access.UserId != actor.UserId ||
|
||||
access.TenantId != actor.TenantId ||
|
||||
!access.HasTenantPermission(BackendPermissions.TenantCrmManage) ||
|
||||
access.DataScope.Mode != DataScopeMode.All)
|
||||
{
|
||||
throw new CrmException("CRM admin access was denied.", "crm_access_denied");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Growth;
|
||||
@@ -14,7 +15,8 @@ namespace Tiku.Infrastructure.Growth;
|
||||
|
||||
public sealed class ReferralService(
|
||||
TikuDbContext dbContext,
|
||||
IReferralQrcodeGenerator qrcodeGenerator) : IReferralService
|
||||
IReferralQrcodeGenerator qrcodeGenerator,
|
||||
ICurrentAccessContext currentAccessContext) : IReferralService
|
||||
{
|
||||
private static readonly HashSet<string> AllowedEventTypes = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -109,7 +111,7 @@ public sealed class ReferralService(
|
||||
referralCode.UserId,
|
||||
membership.Role,
|
||||
user.Name,
|
||||
user.Username,
|
||||
user.UserName,
|
||||
user.Phone
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -121,7 +123,7 @@ public sealed class ReferralService(
|
||||
row.UserId,
|
||||
row.Code,
|
||||
row.Role.ToString(),
|
||||
FirstNonBlank(row.Name, row.Username, row.Phone));
|
||||
FirstNonBlank(row.Name, row.UserName, row.Phone));
|
||||
}
|
||||
|
||||
public async Task<ReferralTrackResult> TrackEventAsync(
|
||||
@@ -655,16 +657,11 @@ public sealed class ReferralService(
|
||||
|
||||
private async Task AssertAdminAsync(ReferralAdminActor actor, CancellationToken cancellationToken)
|
||||
{
|
||||
var isAdmin = await dbContext.TenantMemberships.AnyAsync(
|
||||
item =>
|
||||
item.TenantId == actor.TenantId &&
|
||||
item.UserId == actor.UserId &&
|
||||
item.Status == MembershipStatus.Active &&
|
||||
(item.Role == TenantRole.PlatformAdmin ||
|
||||
item.Role == TenantRole.TenantOwner ||
|
||||
item.Role == TenantRole.TenantAdmin),
|
||||
cancellationToken);
|
||||
if (!isAdmin)
|
||||
var access = await currentAccessContext.GetAsync(cancellationToken);
|
||||
if (!access.IsCurrentTenantMember ||
|
||||
access.TenantId != actor.TenantId ||
|
||||
access.UserId != actor.UserId ||
|
||||
!access.HasTenantPermission(BackendPermissions.TenantCrmManage))
|
||||
{
|
||||
throw new ReferralException("Referral admin access was denied.", "referral_access_denied");
|
||||
}
|
||||
@@ -677,7 +674,7 @@ public sealed class ReferralService(
|
||||
{
|
||||
var user = await dbContext.Users.AsNoTracking()
|
||||
.Where(item => item.Id == referrerUserId)
|
||||
.Select(item => new { item.Name, item.Username, item.Phone })
|
||||
.Select(item => new { item.Name, item.UserName, item.Phone })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
var membership = await dbContext.TenantMemberships.AsNoTracking()
|
||||
.Where(item => item.TenantId == tenantId && item.UserId == referrerUserId)
|
||||
@@ -714,7 +711,7 @@ public sealed class ReferralService(
|
||||
|
||||
return new ReferralStatsItem(
|
||||
referrerUserId,
|
||||
FirstNonBlank(user?.Name, user?.Username, user?.Phone),
|
||||
FirstNonBlank(user?.Name, user?.UserName, user?.Phone),
|
||||
membership.ToString(),
|
||||
inviteCode,
|
||||
leads.Length,
|
||||
|
||||
Reference in New Issue
Block a user