feat: harden SaaS authentication and authorization

This commit is contained in:
2026-07-28 12:15:51 +08:00
parent f22f329d33
commit 5d2248efee
123 changed files with 9090 additions and 2822 deletions

View File

@@ -1,6 +1,7 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Tiku.Application.Auth;
using Tiku.Domain.Tenancy;
namespace Tiku.Api.Contracts;
@@ -9,6 +10,8 @@ namespace Tiku.Api.Contracts;
/// </summary>
public sealed class PasswordLoginDto
{
[Required]
public AuthRealm? Realm { get; set; }
/// <summary>
/// 平台控制域名登录时使用的租户代码;自定义域名登录可省略。
/// </summary>
@@ -17,18 +20,21 @@ public sealed class PasswordLoginDto
public string? TenantCode { get; set; }
/// <summary>
/// 手机号,建议前端提交规范化后的中国大陆手机号
/// 账号标识。tenant 可使用手机号platform 可使用邮箱或用户名
/// </summary>
[Required]
[StringLength(320)]
[Description("账号标识。tenant 可使用手机号platform 可使用邮箱或用户名。")]
public string? Identifier { get; set; }
[StringLength(32)]
[Description("手机号,建议前端提交规范化后的中国大陆手机号。")]
public string Phone { get; set; } = string.Empty;
[Description("兼容手机号字段;新客户端应使用 identifier。")]
public string? Phone { get; set; }
/// <summary>
/// 用户密码。
/// </summary>
[Required]
[StringLength(128, MinimumLength = 6)]
[StringLength(128, MinimumLength = 10)]
[Description("用户密码。")]
public string Password { get; set; } = string.Empty;
}
@@ -38,6 +44,8 @@ public sealed class PasswordLoginDto
/// </summary>
public sealed class SmsLoginDto
{
[Required]
public AuthRealm? Realm { get; set; }
/// <summary>
/// 平台控制域名登录时使用的租户代码;自定义域名登录可省略。
/// </summary>
@@ -62,11 +70,29 @@ public sealed class SmsLoginDto
public string Code { get; set; } = string.Empty;
}
public sealed class SendSmsCodeDto
{
[Required]
public AuthRealm? Realm { get; set; }
[StringLength(100)]
public string? TenantCode { get; set; }
[Required]
[StringLength(32)]
public string Phone { get; set; } = string.Empty;
[StringLength(256)]
public string? DeviceId { get; set; }
}
/// <summary>
/// OAuth code 登录请求。
/// </summary>
public sealed class OAuthCodeDto
{
[Required]
public AuthRealm? Realm { get; set; }
/// <summary>
/// 平台控制域名登录时使用的租户代码;自定义域名登录可省略。
/// </summary>
@@ -135,10 +161,12 @@ public sealed class AuthenticatedUserDto
/// </summary>
public string? Name { get; init; }
public AuthRealm Realm { get; init; }
/// <summary>
/// 当前登录租户成员摘要。
/// </summary>
public TenantMembershipSummary Tenant { get; init; } = default!;
public TenantMembershipSummary? Tenant { get; init; }
/// <summary>
/// access token 和 refresh token。
@@ -153,8 +181,52 @@ public sealed class AuthenticatedUserDto
Phone = user.Phone,
Email = user.Email,
Name = user.Name,
Realm = user.Realm,
Tenant = user.Tenant,
Tokens = user.Tokens
};
}
}
public sealed class AuthenticationResultDto
{
public AuthenticationStatus Status { get; init; }
public AuthenticatedUserDto? User { get; init; }
public string? ChallengeToken { get; init; }
public DateTimeOffset? ChallengeExpiresAt { get; init; }
public static AuthenticationResultDto FromApplication(AuthenticationResult result) => new()
{
Status = result.Status,
User = result.User is null ? null : AuthenticatedUserDto.FromApplication(result.User),
ChallengeToken = result.ChallengeToken,
ChallengeExpiresAt = result.ChallengeExpiresAt
};
}
public sealed class MfaChallengeDto
{
[Required]
[StringLength(2048)]
public string ChallengeToken { get; set; } = string.Empty;
[StringLength(64)]
public string? Code { get; set; }
}
public sealed class MfaConfirmDto
{
public AuthenticationResultDto Authentication { get; init; } = default!;
public IReadOnlyList<string> RecoveryCodes { get; init; } = [];
}
public sealed class RequiredPasswordChangeDto
{
[Required]
[StringLength(2048)]
public string ChallengeToken { get; set; } = string.Empty;
[Required]
[StringLength(128, MinimumLength = 10)]
public string NewPassword { get; set; } = string.Empty;
}

View File

@@ -97,19 +97,6 @@ public sealed class TenantAdminAuditLogQueryDto
}
}
public sealed class TenantAdminRoleTemplateQueryDto
{
public string? Status { get; set; }
[Range(1, 500)]
public int? Limit { get; set; }
public TenantAdminRoleTemplateFilter ToFilter()
{
return new TenantAdminRoleTemplateFilter(Status, Limit);
}
}
public sealed class TenantAdminBadgeQueryDto
{
public string? Category { get; set; }
@@ -324,13 +311,11 @@ public sealed class UpsertTenantAdminMemberDto
public TenantAdminUserLookupDto User { get; set; } = new();
public string? Role { get; set; }
public string? Status { get; set; }
public Guid? RoleTemplateId { get; set; }
public JsonElement Permissions { get; set; } = JsonDefaults.Object();
public string? PrimaryRole { get; set; }
public UpsertTenantAdminMemberCommand ToCommand()
{
return new UpsertTenantAdminMemberCommand(MembershipId, User.ToCommand(), Role, Status, RoleTemplateId, Permissions, PrimaryRole);
return new UpsertTenantAdminMemberCommand(MembershipId, User.ToCommand(), Role, Status, PrimaryRole);
}
}
@@ -340,45 +325,6 @@ public sealed class DisableTenantAdminMemberDto
public Guid MembershipId { get; set; }
}
public sealed class UpsertTenantAdminRoleTemplateDto
{
public Guid? Id { get; set; }
public string? Code { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }
public string? BaseRole { get; set; }
public string? Status { get; set; }
public JsonElement Permissions { get; set; } = JsonDefaults.Object();
public JsonElement MenuPermissions { get; set; } = JsonDefaults.Object();
public JsonElement ModulePermissions { get; set; } = JsonDefaults.Object();
public JsonElement FieldPermissions { get; set; } = JsonDefaults.Object();
public JsonElement DataScope { get; set; } = JsonDefaults.Object();
public int? Order { get; set; }
public UpsertTenantAdminRoleTemplateCommand ToCommand()
{
return new UpsertTenantAdminRoleTemplateCommand(
Id,
Code,
Name,
Description,
BaseRole,
Status,
Permissions,
MenuPermissions,
ModulePermissions,
FieldPermissions,
DataScope,
Order);
}
}
public sealed class DisableTenantAdminRoleTemplateDto
{
[Required]
public Guid RoleTemplateId { get; set; }
}
public sealed class UpsertTenantBrandingDto
{
public required string BrandName { get; set; }