diff --git a/src/Meezi.API/Services/AuthService.cs b/src/Meezi.API/Services/AuthService.cs index 1f00d5f..ba5b545 100644 --- a/src/Meezi.API/Services/AuthService.cs +++ b/src/Meezi.API/Services/AuthService.cs @@ -253,7 +253,9 @@ public class AuthService : IAuthService if (employee?.Cafe is null) return (false, null, "NOT_FOUND", "User no longer exists."); - await _refreshTokenStore.RevokeAsync(request.RefreshToken, cancellationToken); + // Note: we intentionally do NOT revoke the presented refresh token here. + // It is reused (with a slid TTL) so concurrent refreshes from multiple + // tabs/devices stay valid instead of racing each other into a logout. var allMemberships = await _db.Employees .Include(e => e.Cafe) @@ -265,7 +267,9 @@ public class AuthService : IAuthService .Select(e => new CafeMembershipDto(e.CafeId, e.Cafe!.Name, e.Role.ToString(), e.Cafe.PlanTier.ToString())) .ToList(); - var tokens = await IssueTokensAsync(employee, employee.Cafe, membershipDtos, payload.ActiveBranchId, cancellationToken); + var tokens = await IssueTokensAsync( + employee, employee.Cafe, membershipDtos, payload.ActiveBranchId, cancellationToken, + existingRefreshToken: request.RefreshToken); return (true, tokens, null, null); } @@ -510,12 +514,18 @@ public class AuthService : IAuthService Core.Entities.Cafe cafe, List? memberships, string? requestedBranchId, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + string? existingRefreshToken = null) { var resolution = await ResolveBranchAsync(employee, cafe, requestedBranchId, cancellationToken); var accessToken = _jwtTokenService.CreateAccessToken(employee, cafe, resolution.EffectiveRole, resolution.ActiveBranchId); - var refreshToken = _jwtTokenService.CreateRefreshToken(); + // On refresh, reuse the caller's refresh token (and slide its TTL below) instead + // of minting a new one. A café often runs POS + KDS + queue display at once; if + // refresh rotated the token, the first refresh would revoke it and every other + // concurrent refresh would get INVALID_TOKEN → forced logout → OTP storm. + // Mint a fresh token only on a real login (existingRefreshToken == null). + var refreshToken = existingRefreshToken ?? _jwtTokenService.CreateRefreshToken(); var refreshDays = _configuration.GetValue("Jwt:RefreshTokenExpiryDays", 30); await _refreshTokenStore.StoreAsync(