Files
tiku-backend.net/Tiku.Infrastructure/Auth/OwnerActivationService.cs

97 lines
4.5 KiB
C#

using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Auth;
using Tiku.Application.Security;
using Tiku.Domain.Identity;
using Tiku.Domain.Operations;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.Auth;
internal sealed class OwnerActivationService(
ITenantExecutionScope tenantExecutionScope) : IOwnerActivationService
{
public Task CompleteAsync(
CompleteOwnerActivationRequest request,
CancellationToken cancellationToken = default) =>
tenantExecutionScope.ExecuteAsync(
new SystemScopeRequest(
null,
SystemScopeCallerType.Anonymous,
nameof(OwnerActivationService),
"Complete tenant owner activation",
request.ActivationId.ToString("N"),
true),
async (services, token) =>
{
var dbContext = services.GetRequiredService<TikuDbContext>();
var grant = await dbContext.TenantOwnerActivationGrants
.AsNoTracking()
.SingleOrDefaultAsync(value => value.Id == request.ActivationId, token)
?? throw Error("Owner activation was not found.", "owner_activation_invalid");
var now = DateTimeOffset.UtcNow;
var tokenHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(request.Token))).ToLowerInvariant();
if (grant.ConsumedAt.HasValue)
{
throw Error("Owner activation was already consumed.", "owner_activation_consumed");
}
if (grant.ExpiresAt <= now || !CryptographicOperations.FixedTimeEquals(
Convert.FromHexString(grant.TokenHash),
Convert.FromHexString(tokenHash)))
{
throw Error("Owner activation is invalid or expired.", "owner_activation_invalid");
}
var claimed = await dbContext.TenantOwnerActivationGrants
.Where(value => value.Id == grant.Id &&
value.ConsumedAt == null &&
value.ExpiresAt > now &&
value.TokenHash == tokenHash)
.ExecuteUpdateAsync(setters => setters
.SetProperty(value => value.ConsumedAt, now)
.SetProperty(value => value.UpdatedAt, now), token);
if (claimed != 1)
{
throw Error("Owner activation is invalid or already consumed.", "owner_activation_consumed");
}
var userManager = services.GetRequiredService<UserManager<User>>();
var user = await userManager.FindByIdAsync(grant.UserId.ToString())
?? throw Error("Owner account was not found.", "owner_activation_invalid");
if (await userManager.HasPasswordAsync(user))
{
throw Error("Owner account was already activated.", "owner_activation_consumed");
}
var result = await userManager.AddPasswordAsync(user, request.NewPassword);
if (!result.Succeeded)
{
throw Error(
string.Join("; ", result.Errors.Select(error => error.Description)),
"owner_activation_password_invalid");
}
user.ForcePasswordChange = false;
var updateResult = await userManager.UpdateAsync(user);
if (!updateResult.Succeeded)
{
throw Error("Owner account activation could not be completed.", "owner_activation_failed");
}
await userManager.UpdateSecurityStampAsync(user);
dbContext.AuditLogs.Add(new AuditLog
{
TenantId = grant.TenantId,
ActorUserId = grant.UserId,
Action = "tenant.owner.activated",
TargetType = "users",
TargetId = grant.UserId.ToString()
});
await dbContext.SaveChangesAsync(token);
},
cancellationToken);
private static OwnerActivationException Error(string message, string code) => new(message, code);
}