forked from xiongyuxing/tiku-backend.net
178 lines
7.4 KiB
C#
178 lines
7.4 KiB
C#
using System.Data;
|
|
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Storage;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Domain.Identity;
|
|
using Tiku.Domain.Operations;
|
|
using Tiku.Domain.Platform;
|
|
using Tiku.Infrastructure.Persistence;
|
|
|
|
namespace Tiku.Infrastructure.Bootstrap;
|
|
|
|
public sealed record PlatformAdminBootstrapOptions(
|
|
string Email,
|
|
string TemporaryPassword,
|
|
string? DisplayName = null);
|
|
|
|
public sealed record PlatformAdminBootstrapResult(Guid UserId, Guid RoleId, string Email);
|
|
|
|
public sealed class PlatformAdminBootstrapper(
|
|
TikuDbContext dbContext,
|
|
UserManager<User> userManager)
|
|
{
|
|
public const string SuperAdminRoleCode = "platform_super_admin";
|
|
|
|
public async Task<PlatformAdminBootstrapResult> BootstrapAsync(
|
|
PlatformAdminBootstrapOptions options,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
var email = options.Email.Trim();
|
|
if (email.Length == 0)
|
|
{
|
|
throw new ArgumentException("Platform administrator email is required.", nameof(options));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(options.TemporaryPassword))
|
|
{
|
|
throw new ArgumentException("Platform administrator temporary password is required.", nameof(options));
|
|
}
|
|
|
|
IDbContextTransaction? transaction = null;
|
|
if (dbContext.Database.IsRelational())
|
|
{
|
|
transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellationToken);
|
|
}
|
|
|
|
await using (transaction)
|
|
{
|
|
var existingAdministrator = await (
|
|
from binding in dbContext.PlatformBackendUserRoles.AsNoTracking()
|
|
join boundRole in dbContext.PlatformBackendRoles.AsNoTracking() on binding.RoleId equals boundRole.Id
|
|
join boundUser in dbContext.Users.AsNoTracking() on binding.UserId equals boundUser.Id
|
|
where boundRole.Status == BackendRoleStatus.Active && boundUser.Status == UserStatus.Active
|
|
select boundUser.Id)
|
|
.AnyAsync(cancellationToken);
|
|
if (existingAdministrator)
|
|
{
|
|
throw new PlatformAdminBootstrapException(
|
|
"A platform administrator already exists. Bootstrap is a one-time operation.",
|
|
"platform_admin_already_exists");
|
|
}
|
|
|
|
var normalizedEmail = userManager.NormalizeEmail(email);
|
|
if (await dbContext.Users.AsNoTracking().AnyAsync(
|
|
user => user.NormalizedEmail == normalizedEmail || user.NormalizedUserName == normalizedEmail,
|
|
cancellationToken))
|
|
{
|
|
throw new PlatformAdminBootstrapException(
|
|
"The bootstrap email is already assigned to a user.",
|
|
"bootstrap_user_already_exists");
|
|
}
|
|
|
|
var user = new User
|
|
{
|
|
Email = email,
|
|
UserName = email,
|
|
Name = string.IsNullOrWhiteSpace(options.DisplayName) ? "Platform Administrator" : options.DisplayName.Trim(),
|
|
EmailConfirmed = true,
|
|
Status = UserStatus.Active,
|
|
ForcePasswordChange = true
|
|
};
|
|
var createResult = await userManager.CreateAsync(user, options.TemporaryPassword);
|
|
if (!createResult.Succeeded)
|
|
{
|
|
var errors = string.Join(", ", createResult.Errors.Select(error => $"{error.Code}: {error.Description}"));
|
|
throw new PlatformAdminBootstrapException(
|
|
$"Platform administrator could not be created: {errors}",
|
|
"bootstrap_user_invalid");
|
|
}
|
|
|
|
var role = new PlatformBackendRole
|
|
{
|
|
Code = SuperAdminRoleCode,
|
|
Name = "Platform Super Administrator",
|
|
Description = "Built-in role with all platform permissions. Created by the one-time bootstrap command.",
|
|
Status = BackendRoleStatus.Active,
|
|
IsSystem = true
|
|
};
|
|
dbContext.PlatformBackendRoles.Add(role);
|
|
|
|
var platformPermissionCodes = BackendPermissions.Platform.ToArray();
|
|
var platformModuleCodes = platformPermissionCodes
|
|
.Select(PermissionModuleCatalog.ResolvePermissionModuleCode)
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToArray();
|
|
var existingModuleCodes = await dbContext.PermissionModules
|
|
.Where(module => platformModuleCodes.Contains(module.Code))
|
|
.Select(module => module.Code)
|
|
.ToArrayAsync(cancellationToken);
|
|
dbContext.PermissionModules.AddRange(platformModuleCodes
|
|
.Except(existingModuleCodes, StringComparer.Ordinal)
|
|
.Select(code => new PermissionModule
|
|
{
|
|
Code = code,
|
|
Name = code,
|
|
Area = BackendPermissionArea.Platform
|
|
}));
|
|
var existingPermissionCodes = await dbContext.BackendPermissions
|
|
.Where(permission => platformPermissionCodes.Contains(permission.Code))
|
|
.Select(permission => permission.Code)
|
|
.ToHashSetAsync(StringComparer.Ordinal, cancellationToken);
|
|
foreach (var permissionCode in platformPermissionCodes.Where(code => !existingPermissionCodes.Contains(code)))
|
|
{
|
|
dbContext.BackendPermissions.Add(new BackendPermission
|
|
{
|
|
Code = permissionCode,
|
|
Name = permissionCode,
|
|
Area = BackendPermissionArea.Platform,
|
|
PermissionModuleCode = PermissionModuleCatalog.ResolvePermissionModuleCode(permissionCode),
|
|
Description = "Built-in platform permission.",
|
|
IsSystem = true
|
|
});
|
|
}
|
|
|
|
dbContext.PlatformBackendRolePermissions.AddRange(
|
|
platformPermissionCodes.Select(permissionCode => new PlatformBackendRolePermission
|
|
{
|
|
RoleId = role.Id,
|
|
PermissionCode = permissionCode
|
|
}));
|
|
dbContext.PlatformBackendUserRoles.Add(new PlatformBackendUserRole
|
|
{
|
|
UserId = user.Id,
|
|
RoleId = role.Id
|
|
});
|
|
dbContext.AuditLogs.Add(new AuditLog
|
|
{
|
|
ActorUserId = user.Id,
|
|
Action = "platform.bootstrap_admin.created",
|
|
TargetType = "users",
|
|
TargetId = user.Id.ToString(),
|
|
Details = JsonSerializer.SerializeToElement(new
|
|
{
|
|
user.Email,
|
|
RoleCode = SuperAdminRoleCode,
|
|
ForcePasswordChange = true,
|
|
LoginMethod = "account_password"
|
|
})
|
|
});
|
|
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
if (transaction is not null)
|
|
{
|
|
await transaction.CommitAsync(cancellationToken);
|
|
}
|
|
|
|
return new PlatformAdminBootstrapResult(user.Id, role.Id, email);
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class PlatformAdminBootstrapException(string message, string code) : InvalidOperationException(message)
|
|
{
|
|
public string Code { get; } = code;
|
|
}
|