From db50810d15bc7dde79884bc3975f33573771d7dc Mon Sep 17 00:00:00 2001 From: warrenchen Date: Wed, 4 Feb 2026 17:45:32 +0900 Subject: [PATCH] Refactor token handling in ConfirmAsync method for improved backward compatibility and introduce FindTokenAsync helper method --- .../Services/NewsletterService.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/MemberCenter.Infrastructure/Services/NewsletterService.cs b/src/MemberCenter.Infrastructure/Services/NewsletterService.cs index e6eefaf..e853d34 100644 --- a/src/MemberCenter.Infrastructure/Services/NewsletterService.cs +++ b/src/MemberCenter.Infrastructure/Services/NewsletterService.cs @@ -74,10 +74,12 @@ public sealed class NewsletterService : INewsletterService public async Task ConfirmAsync(string token) { - var tokenHash = HashToken(token, ConfirmTokenPurpose); - var confirmToken = await _dbContext.UnsubscribeTokens - .Include(t => t.Subscription) - .FirstOrDefaultAsync(t => t.TokenHash == tokenHash && t.ConsumedAt == null); + var confirmToken = await FindTokenAsync(token, ConfirmTokenPurpose); + if (confirmToken is null && token.Contains(' ')) + { + // Backward compatibility: legacy Base64 tokens may decode '+' as space in query string. + confirmToken = await FindTokenAsync(token.Replace(' ', '+'), ConfirmTokenPurpose); + } if (confirmToken?.Subscription is null) { @@ -170,7 +172,7 @@ public sealed class NewsletterService : INewsletterService private static string CreateToken() { var bytes = RandomNumberGenerator.GetBytes(32); - return Convert.ToBase64String(bytes); + return Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_'); } private static string HashToken(string token, string purpose) @@ -200,4 +202,12 @@ public sealed class NewsletterService : INewsletterService subscription.Preferences.RootElement.Clone(), subscription.CreatedAt); } + + private Task FindTokenAsync(string token, string purpose) + { + var tokenHash = HashToken(token, purpose); + return _dbContext.UnsubscribeTokens + .Include(t => t.Subscription) + .FirstOrDefaultAsync(t => t.TokenHash == tokenHash && t.ConsumedAt == null); + } }