QrRapido/Controllers/AccountController.cs
Ricardo Carneiro a3238ca6c5 feat: MCP server + landing page + OAuth returnUrl fix
- Add Node.js MCP server (stdio + HTTP/SSE) with generate_qr and generate_pix_qr tools
- Add landing pages PT/EN at /mcp and /mcp/en with hreflang SEO
- Fix OAuth returnUrl via RedirectUri query param (state was always null in callback)
- Fix API key requests bypassing web credit check (use rate limiter instead)
- Add /api/mcp nginx route + Docker Swarm service for n8n cloud integration
- Auto-create API key on first OAuth login with TempData display
- Add UseDefaultFiles() for /mcp → /mcp/index.html serving
- Fix Serilog console log level in Development (was Error, now Info for app logs)
- Add /api/v1/QRManager/me endpoint for API key validation
- Update CI/CD to build and deploy qrrapido-mcp image alongside .NET app

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 21:23:50 -03:00

280 lines
11 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;
namespace QRRapidoApp.Controllers
{
public class AccountController : Controller
{
private readonly IUserService _userService;
private readonly AdDisplayService _adDisplayService;
private readonly ILogger<AccountController> _logger;
private readonly IConfiguration _configuration;
public AccountController(IUserService userService, AdDisplayService adDisplayService,
ILogger<AccountController> logger, IConfiguration configuration)
{
_userService = userService;
_adDisplayService = adDisplayService;
_logger = logger;
_configuration = configuration;
}
[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;
var safeReturn = Url.IsLocalUrl(returnUrl) ? returnUrl : "/";
var properties = new AuthenticationProperties
{
RedirectUri = $"{baseUrl}/Account/GoogleCallback?returnUrl={Uri.EscapeDataString(safeReturn)}"
};
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult LoginMicrosoft(string returnUrl = "/")
{
var baseUrl = _configuration.GetSection("App:BaseUrl").Value;
var safeReturn = Url.IsLocalUrl(returnUrl) ? returnUrl : "/";
var properties = new AuthenticationProperties
{
RedirectUri = $"{baseUrl}/Account/MicrosoftCallback?returnUrl={Uri.EscapeDataString(safeReturn)}"
};
return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme);
}
[HttpGet]
public async Task<IActionResult> GoogleCallback(string returnUrl = "/")
{
var destination = await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme, returnUrl);
return Redirect(destination ?? "/");
}
[HttpGet]
public async Task<IActionResult> MicrosoftCallback(string returnUrl = "/")
{
var destination = await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme, returnUrl);
return Redirect(destination ?? "/");
}
private async Task<string> HandleExternalLoginCallbackAsync(string scheme, string returnUrl = "/")
{
try
{
_adDisplayService.SetViewBagAds(ViewBag);
// Validate returnUrl to prevent open redirect
if (!Url.IsLocalUrl(returnUrl))
returnUrl = "/";
// Prevent redirect loop
if (returnUrl.Contains("/Account/Login", StringComparison.OrdinalIgnoreCase))
returnUrl = "/";
// OAuth middleware already signed in via cookie at /signin-google or /signin-microsoft
// with the provider's claims. Authenticate from that cookie to get the provider identity.
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (!result.Succeeded)
{
_logger.LogWarning("Cookie authentication failed after {Scheme} OAuth callback", 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);
bool isNewUser = user == null;
if (isNewUser)
{
// Fix CS8625: Ensure name is not null
var safeName = !string.IsNullOrEmpty(name) ? name : (email ?? "User");
user = await _userService.CreateUserAsync(email, safeName, scheme, providerId);
// Auto-create first API key so user can start immediately
try
{
var (rawKey, _) = await _userService.GenerateApiKeyAsync(user.Id, "Minha primeira key");
TempData["NewKey"] = rawKey;
TempData["NewKeyName"] = "Minha primeira key";
_logger.LogInformation("Auto-created API key for new user {UserId}", user.Id);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to auto-create API key for new user {UserId}", user.Id);
}
}
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 });
}
}
}