Files
tiku-backend.net/Tiku.Infrastructure/Bootstrap/PlatformAdminBootstrapper.cs

161 lines
6.5 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.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 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,
Module = "platform",
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;
}