using Microsoft.EntityFrameworkCore; using Tiku.Application.Auth; using Tiku.Domain.Identity; using Tiku.Domain.Tenancy; namespace Tiku.Infrastructure.Auth; public sealed partial class AuthService { public async Task ChangeRequiredPasswordAsync( PasswordChangeChallengeRequest request, CancellationToken cancellationToken = default) { var challenge = await FindChallengeAsync( request.ChallengeToken, AuthChallengePurpose.PasswordChange, cancellationToken); var user = await userManager.FindByIdAsync(challenge.UserId.ToString()) ?? throw new InvalidAuthChallengeException(); var resetToken = await userManager.GeneratePasswordResetTokenAsync(user); var reset = await userManager.ResetPasswordAsync(user, resetToken, request.NewPassword); if (!reset.Succeeded) throw new InvalidCredentialsException("invalid_new_password"); user.ForcePasswordChange = false; var updated = await userManager.UpdateAsync(user); if (!updated.Succeeded) throw new InvalidOperationException("Unable to clear the password-change requirement."); await sessionStore.RevokeAllAsync(user.Id, "password_changed", cancellationToken); await ConsumeChallengeAsync(challenge, cancellationToken); await AddSecurityAuditAsync( user.Id, challenge.TenantId, "auth.password.changed", null, request.IpAddress, request.UserAgent, cancellationToken); return await CompleteSuccessfulLoginAsync( challenge.Realm, challenge.TenantId, user, challenge.Provider, user.Email ?? user.Phone ?? user.Id.ToString(), request.IpAddress, request.UserAgent, cancellationToken); } public async Task RequestPasswordResetAsync( PasswordResetCodeRequest request, CancellationToken cancellationToken = default) { var phone = SmsCodeHashing.NormalizePhone(request.Phone); var userId = await dbContext.Users.AsNoTracking() .Where(user => user.Phone == phone && user.Status == UserStatus.Active) .Select(user => (Guid?)user.Id) .SingleOrDefaultAsync(cancellationToken); var eligible = userId.HasValue && await dbContext.TenantMemberships.AsNoTracking().AnyAsync( membership => membership.TenantId == request.TenantId && membership.UserId == userId.Value && membership.Status == MembershipStatus.Active, cancellationToken); if (!eligible) return new SmsSendResult(Guid.NewGuid(), DateTimeOffset.UtcNow.AddMinutes(5)); return await smsVerificationService.CreateCodeAsync( new SendSmsCodeRequest( request.TenantId, phone, SmsPurpose.ResetPassword, request.IpAddress, request.UserAgent, request.DeviceId), cancellationToken); } public async Task ResetPasswordAsync( PasswordResetRequest request, CancellationToken cancellationToken = default) { var phone = SmsCodeHashing.NormalizePhone(request.Phone); var user = await dbContext.Users.SingleOrDefaultAsync( item => item.Phone == phone && item.Status == UserStatus.Active, cancellationToken); if (user is null || !await dbContext.TenantMemberships.AnyAsync( membership => membership.TenantId == request.TenantId && membership.UserId == user.Id && membership.Status == MembershipStatus.Active, cancellationToken)) throw new InvalidCredentialsException(); await smsVerificationService.VerifyCodeAsync( request.TenantId, phone, SmsPurpose.ResetPassword, request.Code, cancellationToken); var token = await userManager.GeneratePasswordResetTokenAsync(user); var reset = await userManager.ResetPasswordAsync(user, token, request.NewPassword); if (!reset.Succeeded) throw new InvalidCredentialsException("invalid_new_password"); user.ForcePasswordChange = false; var updated = await userManager.UpdateAsync(user); if (!updated.Succeeded) throw new InvalidOperationException("Unable to finalize the password reset."); await sessionStore.RevokeAllAsync(user.Id, "password_reset", cancellationToken); await AddSecurityAuditAsync( user.Id, request.TenantId, "auth.password.reset", null, request.IpAddress, request.UserAgent, cancellationToken); } public async Task ChangePasswordAsync( AuthenticatedPasswordChangeRequest request, CancellationToken cancellationToken = default) { var session = await sessionStore.ResolveActiveSessionAsync( request.SessionId, request.UserId, cancellationToken) ?? throw new SessionRevokedException(); var user = await userManager.FindByIdAsync(request.UserId.ToString()) ?? throw new InvalidCredentialsException(); var changed = await userManager.ChangePasswordAsync(user, request.CurrentPassword, request.NewPassword); if (!changed.Succeeded) { var currentPasswordInvalid = changed.Errors.Any(error => string.Equals(error.Code, "PasswordMismatch", StringComparison.OrdinalIgnoreCase)); throw new InvalidCredentialsException(currentPasswordInvalid ? "invalid_credentials" : "invalid_new_password"); } user.ForcePasswordChange = false; var updated = await userManager.UpdateAsync(user); if (!updated.Succeeded) throw new InvalidOperationException("Unable to finalize the password change."); await sessionStore.RevokeAllAsync(user.Id, "password_changed", cancellationToken); await AddSecurityAuditAsync( user.Id, session.TenantId, "auth.password.changed_authenticated", null, request.IpAddress, request.UserAgent, cancellationToken); return await CompleteSuccessfulLoginAsync( session.Realm, session.TenantId, user, PasswordProvider, user.Email ?? user.Phone ?? user.Id.ToString(), request.IpAddress, request.UserAgent, cancellationToken); } }