332 lines
13 KiB
C#
332 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)
|
|
{
|
|
user = await _userService.CreateUserAsync(email, name ?? email, scheme, providerId);
|
|
}
|
|
else
|
|
{
|
|
await _userService.UpdateLastLoginAsync(user.Id);
|
|
}
|
|
|
|
// Create application claims
|
|
var claims = new List<Claim>
|
|
{
|
|
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
|
new Claim(ClaimTypes.Email, user.Email),
|
|
new Claim(ClaimTypes.Name, user.Name),
|
|
new Claim("Provider", user.Provider),
|
|
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");
|
|
}
|
|
|
|
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; }
|
|
}
|
|
} |