Added authentication
This commit is contained in:
@@ -2,11 +2,15 @@
|
||||
using System.Xml.Linq;
|
||||
using EonaCat.DoxaApi.Attributes;
|
||||
using EonaCat.DoxaApi.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
|
||||
namespace EonaCat.DoxaApi.Generation
|
||||
{
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||
|
||||
public sealed class ApiDocumentGenerator
|
||||
{
|
||||
private readonly IReadOnlyList<ActionDescriptor> _actions;
|
||||
@@ -80,6 +84,12 @@ namespace EonaCat.DoxaApi.Generation
|
||||
}
|
||||
|
||||
doc.Schemas = schemaRegistry;
|
||||
doc.SecuritySchemes = _options.SecuritySchemes;
|
||||
doc.Features = new ApiFeatureFlags
|
||||
{
|
||||
MockServer = _options.EnableMockServer,
|
||||
Console = _options.EnableConsole
|
||||
};
|
||||
|
||||
return doc;
|
||||
}
|
||||
@@ -112,6 +122,52 @@ namespace EonaCat.DoxaApi.Generation
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines which security schemes apply to an action, in priority order:
|
||||
/// 1. Explicit [DoxaApiAllowAnonymous] on method or class -> no security.
|
||||
/// 2. Explicit [DoxaApiAuth("schemeId")] on method (falls back to class) -> those schemes.
|
||||
/// 3. Standard ASP.NET Core [Authorize] on method or class, combined with
|
||||
/// DoxaApiOptions.DefaultSecurityScheme if one is configured.
|
||||
/// 4. Otherwise: anonymous.
|
||||
/// </summary>
|
||||
private List<string> ResolveSecurity(ControllerActionDescriptor cad)
|
||||
{
|
||||
var method = cad.MethodInfo;
|
||||
var controller = cad.ControllerTypeInfo;
|
||||
|
||||
bool anonymous = method.GetCustomAttribute<DoxaApiAllowAnonymousAttribute>() is not null
|
||||
|| controller.GetCustomAttribute<DoxaApiAllowAnonymousAttribute>() is not null
|
||||
|| method.GetCustomAttribute<AllowAnonymousAttribute>() is not null;
|
||||
|
||||
if (anonymous)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
var methodSchemes = method.GetCustomAttributes<DoxaApiAuthAttribute>().Select(a => a.SchemeId).ToList();
|
||||
if (methodSchemes.Count > 0)
|
||||
{
|
||||
return methodSchemes.Where(_options.SecuritySchemes.ContainsKey).ToList();
|
||||
}
|
||||
|
||||
var classSchemes = controller.GetCustomAttributes<DoxaApiAuthAttribute>().Select(a => a.SchemeId).ToList();
|
||||
if (classSchemes.Count > 0)
|
||||
{
|
||||
return classSchemes.Where(_options.SecuritySchemes.ContainsKey).ToList();
|
||||
}
|
||||
|
||||
bool requiresAuth = method.GetCustomAttribute<AuthorizeAttribute>() is not null
|
||||
|| controller.GetCustomAttribute<AuthorizeAttribute>() is not null;
|
||||
|
||||
if (requiresAuth && _options.DefaultSecurityScheme is not null
|
||||
&& _options.SecuritySchemes.ContainsKey(_options.DefaultSecurityScheme))
|
||||
{
|
||||
return new List<string> { _options.DefaultSecurityScheme };
|
||||
}
|
||||
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
private ApiEndpoint? BuildEndpoint(ControllerActionDescriptor cad, SchemaBuilder schemaBuilder)
|
||||
{
|
||||
var httpMethod = cad.ActionConstraints?
|
||||
@@ -136,7 +192,8 @@ namespace EonaCat.DoxaApi.Generation
|
||||
Summary = summaryAttr?.Summary ?? xmlDoc?.Summary ?? HumanizeName(cad.ActionName),
|
||||
Description = descAttr?.Description ?? xmlDoc?.Remarks,
|
||||
Deprecated = obsoleteAttr is not null,
|
||||
Tags = new List<string> { ResolveGroupName(cad) }
|
||||
Tags = new List<string> { ResolveGroupName(cad) },
|
||||
Security = ResolveSecurity(cad)
|
||||
};
|
||||
|
||||
foreach (var param in cad.Parameters)
|
||||
@@ -207,6 +264,12 @@ namespace EonaCat.DoxaApi.Generation
|
||||
endpoint.Responses.Add(new ResponseModel { StatusCode = "400", Description = "Invalid request" });
|
||||
}
|
||||
|
||||
if (endpoint.Security.Count > 0)
|
||||
{
|
||||
endpoint.Responses.Add(new ResponseModel { StatusCode = "401", Description = "Unauthorized - missing or invalid credentials" });
|
||||
endpoint.Responses.Add(new ResponseModel { StatusCode = "403", Description = "Forbidden - insufficient permissions" });
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@@ -275,4 +338,4 @@ namespace EonaCat.DoxaApi.Generation
|
||||
return reader;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
namespace EonaCat.DoxaApi.Generation
|
||||
using EonaCat.DoxaApi.Models;
|
||||
|
||||
namespace EonaCat.DoxaApi.Generation
|
||||
{
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||
|
||||
public sealed class DoxaApiOptions
|
||||
{
|
||||
public string Title { get; set; } = "DoxaApi Documentation";
|
||||
@@ -9,5 +14,108 @@
|
||||
public List<string> Servers { get; set; } = new();
|
||||
public string Theme { get; set; } = "auto";
|
||||
public string AccentColor { get; set; } = "#6366f1";
|
||||
|
||||
/// <summary>
|
||||
/// Security schemes available to this API, keyed by scheme id. Populate via
|
||||
/// <see cref="AddApiKeyAuth"/>, <see cref="AddBearerAuth"/>, <see cref="AddBasicAuth"/>,
|
||||
/// or <see cref="AddOAuth2ClientCredentials"/>, or add custom entries directly.
|
||||
/// </summary>
|
||||
public Dictionary<string, SecuritySchemeModel> SecuritySchemes { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// If set, endpoints without an explicit [DoxaApiAuth]/[Authorize]/[DoxaApiAllowAnonymous]
|
||||
/// marker are assumed to require this scheme. Leave null to assume anonymous by default.
|
||||
/// </summary>
|
||||
public string? DefaultSecurityScheme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, exposes a mock-response endpoint (/{prefix}/mock/...) that returns
|
||||
/// schema-shaped fake JSON for any documented operation without touching real
|
||||
/// controller code - useful for frontend teams working ahead of a backend.
|
||||
/// </summary>
|
||||
public bool EnableMockServer { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When true, exposes a /{prefix}/console endpoint for running ad-hoc HTTP requests
|
||||
/// (method/url/headers/body) against the API, independent of any documented operation.
|
||||
/// </summary>
|
||||
public bool EnableConsole { get; set; } = true;
|
||||
|
||||
public DoxaApiOptions AddApiKeyAuth(string id, string headerOrQueryName, string @in = "header", string? description = null)
|
||||
{
|
||||
SecuritySchemes[id] = new SecuritySchemeModel
|
||||
{
|
||||
Id = id,
|
||||
Type = "apiKey",
|
||||
In = @in,
|
||||
Name = headerOrQueryName,
|
||||
Description = description
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public DoxaApiOptions AddBearerAuth(string id = "bearer", string bearerFormat = "JWT", string? description = null)
|
||||
{
|
||||
SecuritySchemes[id] = new SecuritySchemeModel
|
||||
{
|
||||
Id = id,
|
||||
Type = "http",
|
||||
Scheme = "bearer",
|
||||
BearerFormat = bearerFormat,
|
||||
Description = description ?? "Bearer token authentication"
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public DoxaApiOptions AddBasicAuth(string id = "basic", string? description = null)
|
||||
{
|
||||
SecuritySchemes[id] = new SecuritySchemeModel
|
||||
{
|
||||
Id = id,
|
||||
Type = "http",
|
||||
Scheme = "basic",
|
||||
Description = description ?? "HTTP Basic authentication"
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public DoxaApiOptions AddOAuth2ClientCredentials(string id, string tokenUrl, Dictionary<string, string>? scopes = null, string? description = null)
|
||||
{
|
||||
SecuritySchemes[id] = new SecuritySchemeModel
|
||||
{
|
||||
Id = id,
|
||||
Type = "oauth2",
|
||||
Description = description ?? "OAuth 2.0 client credentials flow",
|
||||
Flows = new OAuthFlowsModel
|
||||
{
|
||||
ClientCredentials = new OAuthFlowModel
|
||||
{
|
||||
TokenUrl = tokenUrl,
|
||||
Scopes = scopes ?? new Dictionary<string, string>()
|
||||
}
|
||||
}
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public DoxaApiOptions AddOAuth2AuthorizationCode(string id, string authorizationUrl, string tokenUrl, Dictionary<string, string>? scopes = null, string? description = null)
|
||||
{
|
||||
SecuritySchemes[id] = new SecuritySchemeModel
|
||||
{
|
||||
Id = id,
|
||||
Type = "oauth2",
|
||||
Description = description ?? "OAuth 2.0 authorization code flow",
|
||||
Flows = new OAuthFlowsModel
|
||||
{
|
||||
AuthorizationCode = new OAuthFlowModel
|
||||
{
|
||||
AuthorizationUrl = authorizationUrl,
|
||||
TokenUrl = tokenUrl,
|
||||
Scopes = scopes ?? new Dictionary<string, string>()
|
||||
}
|
||||
}
|
||||
};
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using EonaCat.DoxaApi.Models;
|
||||
|
||||
namespace EonaCat.DoxaApi.Generation
|
||||
{
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||
|
||||
/// <summary>
|
||||
/// Produces schema-shaped, deterministic-but-plausible fake JSON values for a given
|
||||
/// <see cref="SchemaModel"/>. Used by the built-in mock server so that frontend teams
|
||||
/// can develop against an API before its real implementation exists.
|
||||
/// </summary>
|
||||
public static class MockResponseGenerator
|
||||
{
|
||||
public static object? Generate(SchemaModel? schema, ApiDocument doc, int depth = 0, HashSet<string>? seen = null)
|
||||
{
|
||||
seen ??= new HashSet<string>();
|
||||
if (schema is null || depth > 8)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (schema.RefName is not null)
|
||||
{
|
||||
if (!seen.Add(schema.RefName))
|
||||
{
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
if (doc.Schemas.TryGetValue(schema.RefName, out var resolved))
|
||||
{
|
||||
return Generate(resolved, doc, depth + 1, seen);
|
||||
}
|
||||
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
switch (schema.Type)
|
||||
{
|
||||
case "string":
|
||||
return MockString(schema.Format);
|
||||
case "integer":
|
||||
return RandomInt(depth);
|
||||
case "number":
|
||||
return Math.Round(RandomInt(depth) + 0.5, 2);
|
||||
case "boolean":
|
||||
return depth % 2 == 0;
|
||||
case "enum":
|
||||
return schema.EnumValues?.Count > 0 ? schema.EnumValues[0] : "value";
|
||||
case "array":
|
||||
return new List<object?>
|
||||
{
|
||||
Generate(schema.Items, doc, depth + 1, seen),
|
||||
Generate(schema.Items, doc, depth + 1, seen)
|
||||
};
|
||||
case "object":
|
||||
{
|
||||
if (schema.Properties is null)
|
||||
{
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
var obj = new Dictionary<string, object?>();
|
||||
foreach (var (key, propSchema) in schema.Properties)
|
||||
{
|
||||
obj[key] = Generate(propSchema, doc, depth + 1, seen);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static object MockString(string? format) => format switch
|
||||
{
|
||||
"date-time" => DateTime.UtcNow.ToString("O"),
|
||||
"date" => DateOnly.FromDateTime(DateTime.UtcNow).ToString("yyyy-MM-dd"),
|
||||
"time" => TimeOnly.FromDateTime(DateTime.UtcNow).ToString("HH:mm:ss"),
|
||||
"uuid" => Guid.NewGuid().ToString(),
|
||||
"uri" => "https://example.com/resource",
|
||||
_ => "string"
|
||||
};
|
||||
|
||||
private static int RandomInt(int depth) => 1 + depth;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ using EonaCat.DoxaApi.Models;
|
||||
|
||||
namespace EonaCat.DoxaApi.Generation
|
||||
{
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||
public sealed class SchemaBuilder
|
||||
{
|
||||
private readonly Dictionary<string, SchemaModel> _registry;
|
||||
|
||||
@@ -4,6 +4,9 @@ using System.Xml.Linq;
|
||||
|
||||
namespace EonaCat.DoxaApi.Generation
|
||||
{
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||
|
||||
internal sealed class MethodXmlDoc
|
||||
{
|
||||
public string? Summary { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user