QrRapido/Controllers/AccountController.cs
Ricardo Carneiro dbd3c8851b
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m50s
Deploy QR Rapido / build-and-push (push) Successful in 16m49s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m12s
fix: ajuste de warnings e combos
2026-01-25 00:44:34 -03:00

335 lines
13 KiB
C#

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Services;
using System.Security.Claims;
using System.Text.Json;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
namespace QRRapidoApp.Controllers
{
public class AccountController : Controller
{
private readonly IUserService _userService;
private readonly AdDisplayService _adDisplayService;
private readonly ILogger<AccountController> _logger;
private readonly IConfiguration _configuration;
private readonly IDataProtector _protector;
public AccountController(IUserService userService, AdDisplayService adDisplayService,
ILogger<AccountController> logger, IConfiguration configuration,
IDataProtectionProvider dataProtection)
{
_userService = userService;
_adDisplayService = adDisplayService;
_logger = logger;
_configuration = configuration;
_protector = dataProtection.CreateProtector("OAuth.StateProtection");
}
[HttpGet]
public IActionResult Login(string returnUrl = "/")
{
_adDisplayService.SetViewBagAds(ViewBag);
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpGet]
public IActionResult LoginGoogle(string returnUrl = "/")
{
var baseUrl = _configuration.GetSection("App:BaseUrl").Value;
// Criar state com dados criptografados em vez de sessão
var stateData = new OAuthStateData
{
ReturnUrl = returnUrl,
Nonce = Guid.NewGuid().ToString(),
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
var stateJson = JsonSerializer.Serialize(stateData);
var protectedState = _protector.Protect(stateJson);
var encodedState = Convert.ToBase64String(Encoding.UTF8.GetBytes(protectedState));
var properties = new AuthenticationProperties
{
RedirectUri = $"{baseUrl}{Url.Action("GoogleCallback")}",
Items = { { "state", encodedState } }
};
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult LoginMicrosoft(string returnUrl = "/")
{
var baseUrl = _configuration.GetSection("App:BaseUrl").Value;
// Mesmo processo para Microsoft
var stateData = new OAuthStateData
{
ReturnUrl = returnUrl,
Nonce = Guid.NewGuid().ToString(),
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
var stateJson = JsonSerializer.Serialize(stateData);
var protectedState = _protector.Protect(stateJson);
var encodedState = Convert.ToBase64String(Encoding.UTF8.GetBytes(protectedState));
var redirectUrl = returnUrl == "/"
? $"{baseUrl}/Account/MicrosoftCallback"
: $"{baseUrl}/Account/MicrosoftCallback";
var properties = new AuthenticationProperties
{
RedirectUri = redirectUrl,
Items = { { "state", encodedState } }
};
return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme);
}
[HttpGet]
public async Task<IActionResult> GoogleCallback(string state = null)
{
var returnUrl = await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme, state);
return Redirect(returnUrl ?? "/");
}
[HttpGet]
public async Task<IActionResult> MicrosoftCallback(string state = null)
{
var returnUrl = await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme, state);
return Redirect(returnUrl ?? "/");
}
private async Task<string> HandleExternalLoginCallbackAsync(string scheme, string state = null)
{
try
{
_adDisplayService.SetViewBagAds(ViewBag);
// Recuperar returnUrl do state em vez da sessão
string returnUrl = "/";
if (!string.IsNullOrEmpty(state))
{
try
{
var decodedState = Encoding.UTF8.GetString(Convert.FromBase64String(state));
var unprotectedState = _protector.Unprotect(decodedState);
var stateData = JsonSerializer.Deserialize<OAuthStateData>(unprotectedState);
// Validar timestamp (não mais que 10 minutos)
var maxAge = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - (10 * 60);
if (stateData.Timestamp > maxAge)
{
returnUrl = stateData.ReturnUrl ?? "/";
// Prevent redirect loop to login page
if (returnUrl.Contains("/Account/Login", StringComparison.OrdinalIgnoreCase))
{
returnUrl = "/";
}
}
else
{
_logger.LogWarning($"OAuth state expired for scheme {scheme}");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Failed to decode OAuth state for scheme {scheme}");
}
}
var result = await HttpContext.AuthenticateAsync(scheme);
if (!result.Succeeded)
{
_logger.LogWarning($"External authentication failed for scheme {scheme}");
return "/Account/Login";
}
var email = result.Principal?.FindFirst(ClaimTypes.Email)?.Value;
var name = result.Principal?.FindFirst(ClaimTypes.Name)?.Value;
var providerId = result.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(providerId))
{
_logger.LogWarning($"Missing required claims from {scheme} authentication");
return "/Account/Login";
}
// Find or create user
var user = await _userService.GetUserByProviderAsync(scheme, providerId);
if (user == null)
{
// Fix CS8625: Ensure name is not null
var safeName = !string.IsNullOrEmpty(name) ? name : (email ?? "User");
user = await _userService.CreateUserAsync(email, safeName, scheme, providerId);
}
else
{
await _userService.UpdateLastLoginAsync(user.Id);
}
// Create application claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id ?? string.Empty), // Fix CS8625
new Claim(ClaimTypes.Email, user.Email ?? string.Empty), // Fix CS8625
new Claim(ClaimTypes.Name, user.Name ?? string.Empty), // Fix CS8625
new Claim("Provider", user.Provider ?? string.Empty),
new Claim("IsPremium", user.IsPremium.ToString())
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(30)
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity), authProperties);
return returnUrl;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error in external login callback for {scheme}");
return "/Account/Login";
}
}
[HttpPost]
[Authorize]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Index", "Home");
}
[HttpGet]
[Authorize]
public async Task<IActionResult> Profile()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login");
}
var user = await _userService.GetUserAsync(userId);
if (user == null)
{
return RedirectToAction("Login");
}
// Ensure we are passing a non-null userId
ViewBag.QRHistory = await _userService.GetUserQRHistoryAsync(userId, 10);
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId);
_adDisplayService.SetViewBagAds(ViewBag);
return View(user);
}
[HttpGet]
public async Task<IActionResult> AdFreeStatus()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new AdFreeStatusViewModel
{
IsAdFree = false,
TimeRemaining = 0,
IsPremium = false
});
}
var shouldShowAds = await _adDisplayService.ShouldShowAds(userId);
var isPremium = await _adDisplayService.HasValidPremiumSubscription(userId);
var expiryDate = await _adDisplayService.GetAdFreeExpiryDate(userId);
var status = await _adDisplayService.GetAdFreeStatusAsync(userId);
return Json(new AdFreeStatusViewModel
{
IsAdFree = !shouldShowAds,
TimeRemaining = isPremium ? int.MaxValue : 0,
IsPremium = isPremium,
ExpiryDate = expiryDate,
SessionType = status
});
}
[HttpPost]
[Authorize]
public async Task<IActionResult> ExtendAdFreeTime(int minutes)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Método removido - sem extensão de tempo ad-free
return Json(new { success = false, message = "Feature not available" });
}
[HttpGet]
[Authorize]
public async Task<IActionResult> History()
{
_adDisplayService.SetViewBagAds(ViewBag);
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login");
}
var history = await _userService.GetUserQRHistoryAsync(userId, 50);
return View(history);
}
[HttpPost]
[Authorize]
public async Task<IActionResult> UpdatePreferences(string language)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new { success = false });
}
try
{
var user = await _userService.GetUserAsync(userId);
if (user != null)
{
user.PreferredLanguage = language;
await _userService.UpdateUserAsync(user);
return Json(new { success = true });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error updating preferences for user {userId}");
}
return Json(new { success = false });
}
}
// Classe para dados do state
public class OAuthStateData
{
public string ReturnUrl { get; set; } = "/";
public string Nonce { get; set; } = "";
public long Timestamp { get; set; }
}
}