using System.Text.RegularExpressions; using Foxnouns.Backend.Database; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing.Template; using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers; [ApiController] [Route("/api/internal")] public partial class InternalController(ILogger logger, DatabaseContext db) : ControllerBase { private readonly ILogger _logger = logger.ForContext(); [HttpPost("force-log-out")] [Authenticate] [Authorize("identify")] public async Task ForceLogoutAsync() { var user = HttpContext.GetUser()!; _logger.Information("Invalidating all tokens for user {UserId}", user.Id); await db.Tokens.Where(t => t.UserId == user.Id) .ExecuteUpdateAsync(s => s.SetProperty(t => t.ManuallyExpired, true)); return NoContent(); } [GeneratedRegex(@"(\{\w+\})")] private static partial Regex PathVarRegex(); private static string GetCleanedTemplate(string template) { if (template.StartsWith("api/v2")) template = template["api/v2".Length..]; template = PathVarRegex() .Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}` .Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}` if (template.Contains("{id}")) return template.Split("{id}")[0] + "{id}"; return template; } [HttpPost("request-data")] public async Task GetRequestDataAsync([FromBody] RequestDataRequest req) { var endpoint = GetEndpoint(HttpContext, req.Path, req.Method); if (endpoint == null) throw new ApiError.BadRequest("Path/method combination is invalid"); var actionDescriptor = endpoint.Metadata.GetMetadata(); var template = actionDescriptor?.AttributeRouteInfo?.Template; if (template == null) throw new FoxnounsError("Template value was null on valid endpoint"); template = GetCleanedTemplate(template); // If no token was supplied, or it isn't valid base 64, return a null user ID (limiting by IP) if (!AuthUtils.TryParseToken(req.Token, out var rawToken)) return Ok(new RequestDataResponse(null, template)); var userId = await db.GetTokenUserId(rawToken); return Ok(new RequestDataResponse(userId, template)); } public record RequestDataRequest(string? Token, string Method, string Path); public record RequestDataResponse( Snowflake? UserId, string Template); private static RouteEndpoint? GetEndpoint(HttpContext httpContext, string url, string requestMethod) { var endpointDataSource = httpContext.RequestServices.GetService(); if (endpointDataSource == null) return null; var endpoints = endpointDataSource.Endpoints.OfType(); foreach (var endpoint in endpoints) { if (endpoint.RoutePattern.RawText == null) continue; var templateMatcher = new TemplateMatcher(TemplateParser.Parse(endpoint.RoutePattern.RawText), new RouteValueDictionary()); if (!templateMatcher.TryMatch(url, new())) continue; var httpMethodAttribute = endpoint.Metadata.GetMetadata(); if (httpMethodAttribute != null && !httpMethodAttribute.HttpMethods.Any(x => x.Equals(requestMethod, StringComparison.OrdinalIgnoreCase))) continue; return endpoint; } return null; } }