This commit is contained in:
warrenchen 2026-02-10 18:05:07 +09:00
commit 8c6e3c550f

View File

@ -81,10 +81,12 @@ public sealed class NewsletterService : INewsletterService
public async Task<SubscriptionDto?> 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)
{
@ -213,7 +215,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)
@ -243,4 +245,12 @@ public sealed class NewsletterService : INewsletterService
subscription.Preferences.RootElement.Clone(),
subscription.CreatedAt);
}
private Task<UnsubscribeToken?> 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);
}
}