mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 09:31:08 +00:00
8e7c602f51
AZ-535: POST /logout (caller's session), /logout/all (all sessions for user),
admin POST /sessions/{sid}/revoke, and verifier-only GET /sessions/revoked
snapshot. New Service role gates the snapshot. Idempotent revoke; reason +
revoked_by_user_id audited per row.
AZ-533: POST /sessions/mission mints a long-lived no-refresh ES256 token bound
to one aircraft + one mission. Audience narrowed to satellite-provider, hard
12 h cap, persisted as class='mission' so the existing logout/revoke surface
covers it. Successful CompanionPC /login or /token/refresh auto-revokes that
aircraft's open mission session (post-flight reconnect).
Schema: 09_sessions_logout_and_mission.sql adds revoked_by_user_id, class,
aircraft_id; drops NOT NULL on refresh_hash for mission rows; adds two partial
indexes for the auto-revoke and snapshot hot paths.
Tests: 13 new e2e tests, all green; full suite 75/76 (1 pre-existing flake in
PasswordHashingTests AC5 timing assertion, unrelated to this batch).
Co-authored-by: Cursor <cursoragent@cursor.com>
60 lines
2.3 KiB
C#
60 lines
2.3 KiB
C#
using System.Globalization;
|
|
using Microsoft.AspNetCore.Diagnostics;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace Azaion.Common;
|
|
|
|
public class BusinessExceptionHandler(ILogger<BusinessExceptionHandler> logger) : IExceptionHandler
|
|
{
|
|
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
|
|
{
|
|
if (exception is BusinessException ex)
|
|
{
|
|
logger.LogWarning(exception, ex.Message);
|
|
httpContext.Response.StatusCode = MapStatusCode(ex.ExceptionEnum);
|
|
httpContext.Response.ContentType = "application/json";
|
|
|
|
if (ex.RetryAfterSeconds is { } retry && retry > 0)
|
|
httpContext.Response.Headers.RetryAfter = retry.ToString(CultureInfo.InvariantCulture);
|
|
|
|
var err = JsonConvert.SerializeObject(new
|
|
{
|
|
ErrorCode = ex.ExceptionEnum,
|
|
ex.Message
|
|
});
|
|
await httpContext.Response.WriteAsync(err, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
if (exception is BadHttpRequestException badReq)
|
|
{
|
|
logger.LogWarning(exception, badReq.Message);
|
|
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
httpContext.Response.ContentType = "application/json";
|
|
|
|
var err = JsonConvert.SerializeObject(new
|
|
{
|
|
ErrorCode = 0,
|
|
badReq.Message
|
|
});
|
|
await httpContext.Response.WriteAsync(err, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static int MapStatusCode(ExceptionEnum kind) => kind switch
|
|
{
|
|
ExceptionEnum.AccountLocked => StatusCodes.Status423Locked,
|
|
ExceptionEnum.LoginRateLimited => StatusCodes.Status429TooManyRequests,
|
|
ExceptionEnum.InvalidRefreshToken => StatusCodes.Status401Unauthorized,
|
|
ExceptionEnum.SessionNotFound => StatusCodes.Status404NotFound,
|
|
ExceptionEnum.InvalidMissionRequest => StatusCodes.Status400BadRequest,
|
|
ExceptionEnum.AircraftNotFound => StatusCodes.Status400BadRequest,
|
|
_ => StatusCodes.Status409Conflict
|
|
};
|
|
}
|