feat: Bao/Iris-Statusrechte + Bao→Iris-Notifications + Agent-Workflow-Übersicht
- Bao darf jetzt Status ändern (neben Iris), Sub-Agents weiterhin nicht - CanEditContent für Inhaltsbearbeitung durch alle bekannten Caller - Bao-Content-Änderungen triggern task_content_changed-Notification an Iris - Bao-Status-Änderungen triggern task_status_changed-Notification an Iris - Iris-Status-Änderungen triggern task_status_changed-Notification an Bao - Neue WorkTask-Felder: IsAgentTask (bool), ExpectedFrom (string) - Agent-Workflow-API: CreateAgentTask, WaitingTasks, AgentOverview - Frontend: Agent-Task-Badge, Iris-Overview-Panel, isBao-Getter - Login-Rate-Limiter mit strukturiertem JSON-Fehlermeldungs-Body - Volume-Name: nexus-postgres → postgres-data (Standardisierung)
This commit is contained in:
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Nexus.Api.DTOs;
|
||||
using Nexus.Api.Integrations;
|
||||
using Nexus.Api.RateLimiting;
|
||||
using Nexus.Api.Services;
|
||||
|
||||
namespace Nexus.Api.Controllers;
|
||||
@@ -14,7 +15,8 @@ public class AuthController(
|
||||
IAuthService authService,
|
||||
IAntiforgery antiforgery,
|
||||
IConfiguration config,
|
||||
IHostEnvironment env) : ControllerBase
|
||||
IHostEnvironment env,
|
||||
LoginAttemptTracker attemptTracker) : ControllerBase
|
||||
{
|
||||
[HttpGet("csrf")]
|
||||
public IActionResult GetCsrfToken()
|
||||
@@ -30,11 +32,38 @@ public class AuthController(
|
||||
if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password))
|
||||
return Results.ValidationProblem(new Dictionary<string, string[]> { ["credentials"] = ["Email and password are required."] });
|
||||
|
||||
var session = await authService.LoginAsync(request, ct);
|
||||
if (session is null) return Results.Unauthorized();
|
||||
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
|
||||
var session = await authService.LoginAsync(request, ct);
|
||||
if (session is null)
|
||||
{
|
||||
var remaining = attemptTracker.RecordFailedAttempt(ip);
|
||||
var retryAfterSeconds = attemptTracker.GetRetryAfterSeconds(ip);
|
||||
|
||||
// Attach remaining info to the 401 response via headers only
|
||||
// (the frontend can also parse the 429 body)
|
||||
HttpContext.Response.Headers["X-RateLimit-Remaining"] = remaining.ToString();
|
||||
HttpContext.Response.Headers["X-RateLimit-Limit"] = "5";
|
||||
if (retryAfterSeconds > 0)
|
||||
HttpContext.Response.Headers["X-RateLimit-Reset"] =
|
||||
DateTimeOffset.UtcNow.AddSeconds(retryAfterSeconds).ToUnixTimeSeconds().ToString();
|
||||
|
||||
// Return a structured body so the frontend can display remaining attempts
|
||||
return Results.Json(new
|
||||
{
|
||||
error = "invalid_credentials",
|
||||
message = "Invalid email or password.",
|
||||
remaining,
|
||||
retryAfterSeconds
|
||||
}, statusCode: 401);
|
||||
}
|
||||
|
||||
// Success — reset attempt counter
|
||||
attemptTracker.Reset(ip);
|
||||
SetRefreshCookie(Response, session.RefreshToken);
|
||||
Response.Headers.CacheControl = "no-store";
|
||||
Response.Headers["X-RateLimit-Remaining"] = "5";
|
||||
Response.Headers["X-RateLimit-Limit"] = "5";
|
||||
return Results.Ok(ToAuthResponse(session));
|
||||
}
|
||||
|
||||
@@ -54,6 +83,8 @@ public class AuthController(
|
||||
|
||||
SetRefreshCookie(Response, session.RefreshToken);
|
||||
Response.Headers.CacheControl = "no-store";
|
||||
Response.Headers["X-RateLimit-Remaining"] = "5";
|
||||
Response.Headers["X-RateLimit-Limit"] = "5";
|
||||
return Results.Ok(ToAuthResponse(session));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user