97 lines
4.5 KiB
C#
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);
|
|
}
|