Merge branch 'feature/change-password'

This commit is contained in:
Chris Chen
2026-06-23 20:36:26 -07:00
21 changed files with 579 additions and 13 deletions
+44
View File
@@ -159,6 +159,50 @@ public class AuthService : IAuthService
}
}
// -------------------------------------------------------------------------
// Change password
// -------------------------------------------------------------------------
public async Task<IdentityResult> ChangePasswordAsync(
string userId, string currentPassword, string newPassword, string? currentRawRefreshToken)
{
var user = await _userManager.FindByIdAsync(userId);
if (user is null)
return IdentityResult.Failed(new IdentityError
{
Code = "UserNotFound",
Description = "User not found.",
});
var result = await _userManager.ChangePasswordAsync(user, currentPassword, newPassword);
if (!result.Succeeded)
return result;
// Revoke the user's other active sessions; keep the current one alive.
var currentHash = currentRawRefreshToken is null
? null
: _tokenService.HashToken(currentRawRefreshToken);
var otherTokens = await _db.RefreshTokens
.Where(rt => rt.UserId == userId
&& rt.RevokedAt == null
&& (currentHash == null || rt.TokenHash != currentHash))
.ToListAsync();
foreach (var token in otherTokens)
token.RevokedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
_audit.Write(
AuditActions.PasswordChanged, AuditCategories.Security, LogLevelEnum.Information,
entityName: nameof(AppUser), entityId: user.Id,
summary: $"Password changed: {user.Email}",
userId: user.Id, userEmail: user.Email);
return result;
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
+15
View File
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Identity;
using ROLAC.API.DTOs.Auth;
using ROLAC.API.Entities;
@@ -30,6 +31,20 @@ public interface IAuthService
/// </summary>
Task LogoutAsync(string rawRefreshToken);
/// <summary>
/// Changes the password for an already-authenticated user. Verifies the current
/// password and enforces the configured Identity password policy via
/// <c>UserManager.ChangePasswordAsync</c>. On success, revokes the user's other
/// active refresh tokens (keeping the one matching <paramref name="currentRawRefreshToken"/>)
/// and writes a security audit entry. Returns the <see cref="IdentityResult"/> so the
/// caller can surface failures; never throws on a bad password.
/// </summary>
Task<IdentityResult> ChangePasswordAsync(
string userId,
string currentPassword,
string newPassword,
string? currentRawRefreshToken);
/// <summary>
/// Builds the UserInfo payload (identity, roles, and effective permissions) for an
/// already-authenticated user. Used by GET /api/auth/me to refresh permissions
@@ -5,7 +5,7 @@ namespace ROLAC.API.Services;
public interface IMealAttendanceService
{
/// <summary>Today's date in the server's local time zone (the church's "current Sunday").</summary>
DateOnly Today { get; }
DateOnly ServiceDay { get; }
/// <summary>Returns the counts for <paramref name="date"/>, creating a zeroed row if none exists.</summary>
Task<AttendanceCountsDto> GetOrCreateAsync(DateOnly date);
@@ -12,7 +12,14 @@ public class MealAttendanceService : IMealAttendanceService
public MealAttendanceService(AppDbContext db) => _db = db;
// Server local time is assumed to match the church's local day.
public DateOnly Today => DateOnly.FromDateTime(DateTime.Now);
public DateOnly ServiceDay
{
get
{
var today = DateOnly.FromDateTime(DateTime.Now);
return today.AddDays(-(int)today.DayOfWeek);
}
}
public async Task<AttendanceCountsDto> GetOrCreateAsync(DateOnly date)
{