diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index ba6ba598f9..71598984bc 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -20,7 +20,7 @@
-
+
diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj
index 64605e0c70..35a98f4491 100644
--- a/src/Exceptionless.Core/Exceptionless.Core.csproj
+++ b/src/Exceptionless.Core/Exceptionless.Core.csproj
@@ -31,11 +31,11 @@
-
-
-
+
+
+
-
+
diff --git a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj
index 1e96286708..61e9c4fa3b 100644
--- a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj
+++ b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj
@@ -7,12 +7,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/Exceptionless.Job/Exceptionless.Job.csproj b/src/Exceptionless.Job/Exceptionless.Job.csproj
index 6c92dd4ff9..134687f082 100644
--- a/src/Exceptionless.Job/Exceptionless.Job.csproj
+++ b/src/Exceptionless.Job/Exceptionless.Job.csproj
@@ -9,15 +9,15 @@
-
+
-
+
-
+
@@ -29,4 +29,4 @@
-
\ No newline at end of file
+
diff --git a/src/Exceptionless.Job/Program.cs b/src/Exceptionless.Job/Program.cs
index 5d2191ab4f..2c04155463 100644
--- a/src/Exceptionless.Job/Program.cs
+++ b/src/Exceptionless.Job/Program.cs
@@ -77,7 +77,7 @@ public static IHostBuilder CreateHostBuilder(string[] args)
c.Enrich.WithMachineName();
if (!String.IsNullOrEmpty(options.ExceptionlessApiKey))
- c.WriteTo.Sink(new ExceptionlessSink(), LogEventLevel.Information);
+ c.WriteTo.Exceptionless(restrictedToMinimumLevel: LogEventLevel.Information);
}, writeToProviders: true)
.ConfigureWebHostDefaults(webBuilder =>
{
diff --git a/src/Exceptionless.Web/Api/ApiEndpoints.cs b/src/Exceptionless.Web/Api/ApiEndpoints.cs
new file mode 100644
index 0000000000..a804c7dd7d
--- /dev/null
+++ b/src/Exceptionless.Web/Api/ApiEndpoints.cs
@@ -0,0 +1,30 @@
+using Exceptionless.Web.Api.Endpoints;
+using Foundatio.Mediator;
+
+namespace Exceptionless.Web.Api;
+
+public static class ApiEndpoints
+{
+ public static WebApplication MapApiEndpoints(this WebApplication app)
+ {
+ app.MapStatusEndpoints();
+ app.MapUtilityEndpoints();
+ app.MapContactEndpoints();
+ app.MapAuthEndpoints();
+ app.MapTokenEndpoints();
+ app.MapWebHookEndpoints();
+ app.MapStripeEndpoints();
+ app.MapSavedViewEndpoints();
+ app.MapUserEndpoints();
+ app.MapProjectEndpoints();
+ app.MapOrganizationEndpoints();
+ app.MapStackEndpoints();
+ app.MapAdminEndpoints();
+ app.MapOAuthApplicationEndpoints();
+ app.MapOAuthEndpoints();
+ app.MapEventEndpoints();
+ app.MapMediatorEndpoints();
+
+ return app;
+ }
+}
diff --git a/src/Exceptionless.Web/Api/Endpoints/AdminEndpoints.cs b/src/Exceptionless.Web/Api/Endpoints/AdminEndpoints.cs
new file mode 100644
index 0000000000..c5990ce9ef
--- /dev/null
+++ b/src/Exceptionless.Web/Api/Endpoints/AdminEndpoints.cs
@@ -0,0 +1,33 @@
+using Exceptionless.Core.Authorization;
+using Exceptionless.Web.Api.Filters;
+using Exceptionless.Web.Api.Messages;
+using Exceptionless.Web.Api.Results;
+using Foundatio.Mediator;
+using HttpIResult = Microsoft.AspNetCore.Http.IResult;
+
+namespace Exceptionless.Web.Api.Endpoints;
+
+public static class AdminEndpoints
+{
+ public static IEndpointRouteBuilder MapAdminEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ var group = endpoints.MapGroup("api/v2/admin")
+ .RequireAuthorization(AuthorizationRoles.GlobalAdminPolicy)
+ .AddEndpointFilter()
+ .ExcludeFromDescription();
+
+ group.MapGet("echo", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync>(new GetAdminEcho(httpContext))).ToHttpResult(resultMapper));
+
+ group.MapPost("change-plan", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string organizationId, string planId)
+ => (await mediator.InvokeAsync>(new AdminChangePlan(organizationId, planId, httpContext))).ToHttpResult(resultMapper));
+
+ group.MapPost("set-bonus", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string organizationId, int bonusEvents, DateTime? expires = null)
+ => (await mediator.InvokeAsync(new AdminSetBonus(organizationId, bonusEvents, expires, httpContext))).ToHttpResult(resultMapper));
+
+ group.MapPost("generate-sample-events", async (IMediator mediator, IMediatorResultMapper resultMapper, int eventCount = 250, int daysBack = 7)
+ => (await mediator.InvokeAsync>(new AdminGenerateSampleEvents(eventCount, daysBack))).ToHttpResult(resultMapper));
+
+ return endpoints;
+ }
+}
diff --git a/src/Exceptionless.Web/Api/Endpoints/AuthEndpoints.cs b/src/Exceptionless.Web/Api/Endpoints/AuthEndpoints.cs
new file mode 100644
index 0000000000..7b1fc2b41a
--- /dev/null
+++ b/src/Exceptionless.Web/Api/Endpoints/AuthEndpoints.cs
@@ -0,0 +1,265 @@
+using Exceptionless.Core.Authorization;
+using Exceptionless.Web.Api.Filters;
+using Exceptionless.Web.Api.Infrastructure;
+using Exceptionless.Web.Api.Results;
+using Exceptionless.Web.Models;
+using Foundatio.Mediator;
+using HttpIResult = Microsoft.AspNetCore.Http.IResult;
+using Microsoft.AspNetCore.Mvc;
+using AuthMessages = Exceptionless.Web.Api.Messages;
+using Exceptionless.Web.Utility.OpenApi;
+
+namespace Exceptionless.Web.Api.Endpoints;
+
+public static class AuthEndpoints
+{
+ public static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ var group = endpoints.MapGroup("api/v2/auth")
+ .RequireAuthorization(AuthorizationRoles.UserPolicy)
+ .AddEndpointFilter()
+ .WithTags("Auth");
+
+ group.MapPost("login", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] Login model) =>
+ {
+ return (await mediator.InvokeAsync>(new AuthMessages.LoginMessage(model, httpContext))).ToHttpResult(resultMapper);
+ })
+ .AllowAnonymous()
+ .Accepts("application/json", "application/*+json")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status401Unauthorized)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Login")
+ .WithDescription("""
+ Log in with your email address and password to generate a token scoped with your users roles.
+
+ ```{ "email": "noreply@exceptionless.io", "password": "exceptionless" }```
+
+ This token can then be used to access the api. You can use this token in the header (bearer authentication)
+ or append it onto the query string: ?access_token=MY_TOKEN
+
+ Please note that you can also use this token on the documentation site by placing it in the
+ headers api_key input box.
+ """)
+ .WithMetadata(new EndpointDocumentation {
+ ResponseDescriptions = new() {
+ ["200"] = "User Authentication Token",
+ ["401"] = "Login failed",
+ ["422"] = "Validation error",
+ }
+ });
+
+ group.MapGet("intercom", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext)
+ => (await mediator.InvokeAsync>(new AuthMessages.GetIntercomToken(httpContext))).ToHttpResult(resultMapper))
+ .Produces()
+ .ProducesProblem(StatusCodes.Status401Unauthorized)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Get the current user's Intercom messenger token.")
+ .WithMetadata(new EndpointDocumentation {
+ ResponseDescriptions = new() {
+ ["200"] = "Intercom messenger token",
+ ["401"] = "User not logged in",
+ ["422"] = "Intercom is not enabled.",
+ }
+ });
+
+ group.MapGet("logout", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext)
+ => (await mediator.InvokeAsync(new AuthMessages.LogoutMessage(httpContext))).ToHttpResult(resultMapper))
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status401Unauthorized)
+ .ProducesProblem(StatusCodes.Status403Forbidden)
+ .WithSummary("Logout the current user and remove the current access token")
+ .WithMetadata(new EndpointDocumentation {
+ ResponseDescriptions = new() {
+ ["200"] = "User successfully logged-out",
+ ["401"] = "User not logged in",
+ ["403"] = "Current action is not supported with user access token",
+ }
+ });
+
+ group.MapPost("signup", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] Signup model) =>
+ {
+ return (await mediator.InvokeAsync>(new AuthMessages.SignupMessage(model, httpContext))).ToHttpResult(resultMapper);
+ })
+ .AllowAnonymous()
+ .Accepts("application/json", "application/*+json")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status401Unauthorized)
+ .ProducesProblem(StatusCodes.Status403Forbidden)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Sign up")
+ .WithMetadata(new EndpointDocumentation {
+ ResponseDescriptions = new() {
+ ["200"] = "User Authentication Token",
+ ["401"] = "Sign-up failed",
+ ["403"] = "Account Creation is currently disabled",
+ ["422"] = "Validation error",
+ }
+ });
+
+ group.MapPost("github", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>
+ {
+ return (await mediator.InvokeAsync>(new AuthMessages.GitHubLogin(value, httpContext))).ToHttpResult(resultMapper);
+ })
+ .AllowAnonymous()
+ .Accepts("application/json", "application/*+json")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status403Forbidden)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Sign in with GitHub")
+ .WithMetadata(new EndpointDocumentation {
+ ResponseDescriptions = new() {
+ ["200"] = "User Authentication Token",
+ ["403"] = "Account Creation is currently disabled",
+ ["422"] = "Validation error",
+ }
+ });
+
+ group.MapPost("google", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>
+ {
+ return (await mediator.InvokeAsync>(new AuthMessages.GoogleLogin(value, httpContext))).ToHttpResult(resultMapper);
+ })
+ .AllowAnonymous()
+ .Accepts("application/json", "application/*+json")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status403Forbidden)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Sign in with Google")
+ .WithMetadata(new EndpointDocumentation {
+ ResponseDescriptions = new() {
+ ["200"] = "User Authentication Token",
+ ["403"] = "Account Creation is currently disabled",
+ ["422"] = "Validation error",
+ }
+ });
+
+ group.MapPost("facebook", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>
+ {
+ return (await mediator.InvokeAsync>(new AuthMessages.FacebookLogin(value, httpContext))).ToHttpResult(resultMapper);
+ })
+ .AllowAnonymous()
+ .Accepts("application/json", "application/*+json")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status403Forbidden)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Sign in with Facebook")
+ .WithMetadata(new EndpointDocumentation {
+ ResponseDescriptions = new() {
+ ["200"] = "User Authentication Token",
+ ["403"] = "Account Creation is currently disabled",
+ ["422"] = "Validation error",
+ }
+ });
+
+ group.MapPost("live", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>
+ {
+ return (await mediator.InvokeAsync>(new AuthMessages.LiveLogin(value, httpContext))).ToHttpResult(resultMapper);
+ })
+ .AllowAnonymous()
+ .Accepts("application/json", "application/*+json")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status403Forbidden)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Sign in with Microsoft")
+ .WithMetadata(new EndpointDocumentation {
+ ResponseDescriptions = new() {
+ ["200"] = "User Authentication Token",
+ ["403"] = "Account Creation is currently disabled",
+ ["422"] = "Validation error",
+ }
+ });
+
+ group.MapPost("unlink/{providerName:minlength(1)}", async (string providerName, IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] ValueFromBody providerUserId)
+ => (await mediator.InvokeAsync>(new AuthMessages.RemoveExternalLogin(providerName, providerUserId, httpContext))).ToHttpResult(resultMapper))
+ .Accepts>("application/json", "application/*+json")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Removes an external login provider from the account")
+ .WithMetadata(new EndpointDocumentation {
+ RequestBodyDescription = "The provider user id.",
+ ParameterDescriptions = new() {
+ ["providerName"] = "The provider name.",
+ },
+ ResponseDescriptions = new() {
+ ["200"] = "User Authentication Token",
+ ["400"] = "Invalid provider name.",
+ }
+ });
+
+ group.MapPost("change-password", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] ChangePasswordModel model) =>
+ {
+ return (await mediator.InvokeAsync>(new AuthMessages.ChangePassword(model, httpContext))).ToHttpResult(resultMapper);
+ })
+ .Accepts("application/json", "application/*+json")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Change password")
+ .WithMetadata(new EndpointDocumentation {
+ ResponseDescriptions = new() {
+ ["200"] = "User Authentication Token",
+ ["422"] = "Validation error",
+ }
+ });
+
+ group.MapGet("check-email-address/{email:minlength(1)}", async (string email, IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext)
+ => (await mediator.InvokeAsync(new AuthMessages.CheckEmailAddress(email, httpContext))).ToHttpResult(resultMapper))
+ .AllowAnonymous()
+ .ExcludeFromDescription();
+
+ group.MapGet("forgot-password/{email:minlength(1)}", async (string email, IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext)
+ => (await mediator.InvokeAsync(new AuthMessages.ForgotPassword(email, httpContext))).ToHttpResult(resultMapper))
+ .AllowAnonymous()
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Forgot password")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["email"] = "The email address.",
+ },
+ ResponseDescriptions = new() {
+ ["200"] = "Forgot password email was sent.",
+ ["400"] = "Invalid email address.",
+ }
+ });
+
+ group.MapPost("reset-password", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] ResetPasswordModel model) =>
+ {
+ return (await mediator.InvokeAsync(new AuthMessages.ResetPassword(model, httpContext))).ToHttpResult(resultMapper);
+ })
+ .AllowAnonymous()
+ .Accepts("application/json", "application/*+json")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Reset password")
+ .WithMetadata(new EndpointDocumentation {
+ ResponseDescriptions = new() {
+ ["200"] = "Password reset email was sent.",
+ ["422"] = "Invalid reset password model.",
+ }
+ });
+
+ group.MapPost("cancel-reset-password/{token:minlength(1)}", async (string token, IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext) =>
+ {
+ var contentTypeResult = ApiValidation.ValidateJsonContentType(httpContext.Request);
+ if (contentTypeResult is not null)
+ return contentTypeResult;
+
+ return (await mediator.InvokeAsync(new AuthMessages.CancelResetPassword(token, httpContext))).ToHttpResult(resultMapper);
+ })
+ .AllowAnonymous()
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Cancel reset password")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["token"] = "The password reset token.",
+ },
+ ResponseDescriptions = new() {
+ ["200"] = "Password reset email was cancelled.",
+ ["400"] = "Invalid password reset token.",
+ }
+ });
+
+ return endpoints;
+ }
+}
diff --git a/src/Exceptionless.Web/Api/Endpoints/ContactEndpoints.cs b/src/Exceptionless.Web/Api/Endpoints/ContactEndpoints.cs
new file mode 100644
index 0000000000..c291652ca5
--- /dev/null
+++ b/src/Exceptionless.Web/Api/Endpoints/ContactEndpoints.cs
@@ -0,0 +1,39 @@
+using Exceptionless.Web.Api.Filters;
+using Exceptionless.Web.Api.Messages;
+using Exceptionless.Web.Models;
+using Foundatio.Mediator;
+using HttpIResult = Microsoft.AspNetCore.Http.IResult;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Exceptionless.Web.Api.Endpoints;
+
+public static class ContactEndpoints
+{
+ public static IEndpointRouteBuilder MapContactEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ endpoints.MapPost("api/v2/contact", async (HttpContext httpContext, IMediator mediator, [FromBody] ContactRequest request)
+ => await mediator.InvokeAsync(new SubmitContactRequest(request, httpContext)))
+ .AddEndpointFilter()
+ .AllowAnonymous()
+ .Accepts("application/json")
+ .Produces(StatusCodes.Status202Accepted)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .ProducesProblem(StatusCodes.Status429TooManyRequests)
+ .ProducesProblem(StatusCodes.Status503ServiceUnavailable)
+ .ExcludeFromDescription();
+
+ endpoints.MapPost("api/v2/contact", async (HttpContext httpContext, IMediator mediator, [FromForm] ContactRequest request)
+ => await mediator.InvokeAsync(new SubmitContactRequest(request, httpContext)))
+ .AddEndpointFilter()
+ .AllowAnonymous()
+ .DisableAntiforgery()
+ .Accepts("application/x-www-form-urlencoded", "multipart/form-data")
+ .Produces(StatusCodes.Status202Accepted)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .ProducesProblem(StatusCodes.Status429TooManyRequests)
+ .ProducesProblem(StatusCodes.Status503ServiceUnavailable)
+ .ExcludeFromDescription();
+
+ return endpoints;
+ }
+}
diff --git a/src/Exceptionless.Web/Api/Endpoints/EventEndpoints.cs b/src/Exceptionless.Web/Api/Endpoints/EventEndpoints.cs
new file mode 100644
index 0000000000..fd62142c28
--- /dev/null
+++ b/src/Exceptionless.Web/Api/Endpoints/EventEndpoints.cs
@@ -0,0 +1,927 @@
+using Exceptionless.Core.Authorization;
+using Exceptionless.Core.Models;
+using Exceptionless.Core.Models.Data;
+using Exceptionless.Web.Api.Filters;
+using Exceptionless.Web.Api.Infrastructure;
+using Exceptionless.Web.Api.Messages;
+using Exceptionless.Web.Controllers;
+using Exceptionless.Web.Extensions;
+using Exceptionless.Web.Models;
+using Exceptionless.Web.Utility;
+using Foundatio.Repositories.Models;
+using Exceptionless.Web.Api.Results;
+using Foundatio.Mediator;
+using HttpIResult = Microsoft.AspNetCore.Http.IResult;
+using HttpResults = Microsoft.AspNetCore.Http.Results;
+using Microsoft.AspNetCore.Mvc;
+using Exceptionless.Web.Utility.OpenApi;
+
+namespace Exceptionless.Web.Api.Endpoints;
+
+public static class EventEndpoints
+{
+ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ var group = endpoints.MapGroup("api/v2")
+ .AddEndpointFilter()
+ .WithTags("Event");
+
+ // Count
+ group.MapGet("events/count", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? aggregations = null, string? time = null, string? offset = null, string? mode = null)
+ => (await mediator.InvokeAsync>(new GetEventCount(filter, aggregations, time, offset, mode, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Count")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["aggregations"] = "A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ }
+ });
+
+ group.MapGet("organizations/{organizationId:objectid}/events/count", async (string organizationId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? aggregations = null, string? time = null, string? offset = null, string? mode = null)
+ => (await mediator.InvokeAsync>(new GetEventCountByOrganization(organizationId, filter, aggregations, time, offset, mode, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Count by organization")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["organizationId"] = "The identifier of the organization.",
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["aggregations"] = "A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ }
+ });
+
+ group.MapGet("projects/{projectId:objectid}/events/count", async (string projectId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? aggregations = null, string? time = null, string? offset = null, string? mode = null)
+ => (await mediator.InvokeAsync>(new GetEventCountByProject(projectId, filter, aggregations, time, offset, mode, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Count by project")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["projectId"] = "The identifier of the project.",
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["aggregations"] = "A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If mode is set to stack_new, then additional filters will be added.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ }
+ });
+
+ // Get by id
+ group.MapGet("events/{id:objectid}", async (string id, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, [FromQuery(Name = "expected_stack_id")] string? expectedStackId = null, string? time = null, string? offset = null)
+ => (await mediator.InvokeAsync>(new GetEventById(id, expectedStackId, time, offset, httpContext))).ToHttpResult(resultMapper))
+ .WithName("GetPersistentEventById")
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get by id")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the event.",
+ ["expected_stack_id"] = "Optional stack identifier that the event must belong to.",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "The event does not belong to the expected stack.",
+ ["404"] = "The event occurrence could not be found.",
+ ["426"] = "Unable to view event occurrence due to plan limits.",
+ }
+ });
+
+ // Get all
+ group.MapGet("events", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetAllEvents(filter, sort, time, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get all")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["sort"] = "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ ["426"] = "Unable to view event occurrences for the suspended organization.",
+ }
+ });
+
+ // Get by organization
+ group.MapGet("organizations/{organizationId:objectid}/events", async (string organizationId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetEventsByOrganization(organizationId, filter, sort, time, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get by organization")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["organizationId"] = "The identifier of the organization.",
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["sort"] = "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ ["404"] = "The organization could not be found.",
+ ["426"] = "Unable to view event occurrences for the suspended organization.",
+ }
+ });
+
+ // Get by project
+ group.MapGet("projects/{projectId:objectid}/events", async (string projectId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetEventsByProject(projectId, filter, sort, time, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get by project")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["projectId"] = "The identifier of the project.",
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["sort"] = "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ ["404"] = "The project could not be found.",
+ ["426"] = "Unable to view event occurrences for the suspended organization.",
+ }
+ });
+
+ // Get by stack
+ group.MapGet("stacks/{stackId:objectid}/events", async (string stackId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetEventsByStack(stackId, filter, sort, time, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get by stack")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["stackId"] = "The identifier of the stack.",
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["sort"] = "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ ["404"] = "The stack could not be found.",
+ ["426"] = "Unable to view event occurrences for the suspended organization.",
+ }
+ });
+
+ // Get by reference id
+ group.MapGet("events/by-ref/{referenceId:identifier}", async (string referenceId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetEventsByReferenceId(referenceId, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get by reference id")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["referenceId"] = "An identifier used that references an event instance.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ ["426"] = "Unable to view event occurrences for the suspended organization.",
+ }
+ });
+
+ // Get by reference id + project
+ group.MapGet("projects/{projectId:objectid}/events/by-ref/{referenceId:identifier}", async (string referenceId, string projectId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetEventsByReferenceIdAndProject(referenceId, projectId, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get by reference id")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["referenceId"] = "An identifier used that references an event instance.",
+ ["projectId"] = "The identifier of the project.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ ["404"] = "The project could not be found.",
+ ["426"] = "Unable to view event occurrences for the suspended organization.",
+ }
+ });
+
+ // Sessions by session id
+ group.MapGet("events/sessions/{sessionId:identifier}", async (string sessionId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetEventsBySessionId(sessionId, filter, sort, time, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get a list of all sessions or events by a session id")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["sessionId"] = "An identifier that represents a session of events.",
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["sort"] = "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ ["426"] = "Unable to view event occurrences for the suspended organization.",
+ }
+ });
+
+ // Sessions by session id + project
+ group.MapGet("projects/{projectId:objectid}/events/sessions/{sessionId:identifier}", async (string sessionId, string projectId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetEventsBySessionIdAndProject(sessionId, projectId, filter, sort, time, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get a list of by a session id")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["sessionId"] = "An identifier that represents a session of events.",
+ ["projectId"] = "The identifier of the project.",
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["sort"] = "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ ["404"] = "The project could not be found.",
+ ["426"] = "Unable to view event occurrences for the suspended organization.",
+ }
+ });
+
+ // All sessions
+ group.MapGet("events/sessions", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetSessions(filter, sort, time, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Get a list of all sessions")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["sort"] = "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ }
+ });
+
+ // Sessions by organization
+ group.MapGet("organizations/{organizationId:objectid}/events/sessions", async (string organizationId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetSessionsByOrganization(organizationId, filter, sort, time, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get a list of all sessions")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["organizationId"] = "The identifier of the organization.",
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["sort"] = "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ ["404"] = "The project could not be found.",
+ ["426"] = "Unable to view event occurrences for the suspended organization.",
+ }
+ });
+
+ // Sessions by project
+ group.MapGet("projects/{projectId:objectid}/events/sessions", async (string projectId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null, string? include = null)
+ => (await mediator.InvokeAsync>>(new GetSessionsByProject(projectId, filter, sort, time, offset, mode, page, limit, before, after, include, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.EventsReadPolicy)
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status426UpgradeRequired)
+ .WithSummary("Get a list of all sessions")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["projectId"] = "The identifier of the project.",
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["sort"] = "Controls the sort order that the data is returned in. In this example -date returns the results descending by date.",
+ ["time"] = "The time filter that limits the data being returned to a specific date range.",
+ ["offset"] = "The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.",
+ ["mode"] = "If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.",
+ ["page"] = "The page parameter is used for pagination. This value must be greater than 0.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ ["before"] = "The before parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["after"] = "The after parameter is a cursor used for pagination and defines your place in the list of results.",
+ ["include"] = "Optional response metadata to include. Specify total to include the total result count in response headers.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "Invalid filter.",
+ ["404"] = "The project could not be found.",
+ ["426"] = "Unable to view event occurrences for the suspended organization.",
+ }
+ });
+
+ // User description
+ group.MapPost("events/by-ref/{referenceId:identifier}/user-description", async (string referenceId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, [FromBody] UserDescription description, string? projectId = null)
+ => (await mediator.InvokeAsync(new SetEventUserDescription(referenceId, description, projectId, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .Accepts("application/json", "application/*+json")
+ .Produces(StatusCodes.Status202Accepted)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Set user description")
+ .WithDescription("You can also save an end users contact information and a description of the event. This is really useful for error events as a user can specify reproduction steps in the description.")
+ .WithMetadata(new EndpointDocumentation {
+ RequestBodyDescription = "The user description.",
+ ParameterDescriptions = new() {
+ ["referenceId"] = "An identifier used that references an event instance.",
+ },
+ ResponseDescriptions = new() {
+ ["202"] = "Accepted",
+ ["400"] = "Description must be specified.",
+ ["404"] = "The event occurrence with the specified reference id could not be found.",
+ }
+ });
+
+ group.MapPost("projects/{projectId:objectid}/events/by-ref/{referenceId:identifier}/user-description", async (string referenceId, string projectId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, [FromBody] UserDescription description)
+ => (await mediator.InvokeAsync(new SetEventUserDescription(referenceId, description, projectId, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .Accepts("application/json", "application/*+json")
+ .Produces(StatusCodes.Status202Accepted)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Set user description")
+ .WithDescription("You can also save an end users contact information and a description of the event. This is really useful for error events as a user can specify reproduction steps in the description.")
+ .WithMetadata(new EndpointDocumentation {
+ RequestBodyDescription = "The user description.",
+ ParameterDescriptions = new() {
+ ["referenceId"] = "An identifier used that references an event instance.",
+ ["projectId"] = "The identifier of the project.",
+ },
+ ResponseDescriptions = new() {
+ ["202"] = "Accepted",
+ ["400"] = "Description must be specified.",
+ ["404"] = "The event occurrence with the specified reference id could not be found.",
+ }
+ });
+
+ // Legacy patch (v1)
+ endpoints.MapPatch("api/v1/error/{id:objectid}", async (string id, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, [FromBody] Delta? changes)
+ => changes is null ? HttpResults.Ok() : (await mediator.InvokeAsync(new LegacyPatchEvent(id, changes, httpContext))).ToHttpResult(resultMapper))
+ .Accepts>("application/json", "application/*+json")
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .WithTags("Event")
+ .WithMetadata(
+ new ObsoleteAttribute("Use PATCH /api/v2/events"),
+ new EndpointDocumentation { RequestBodyRequired = true });
+
+ // Heartbeat
+ group.MapGet("events/session/heartbeat", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? id = null, bool close = false)
+ => (await mediator.InvokeAsync(new RecordEventHeartbeat(id, close, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Submit heartbeat")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["id"] = "The session id or user id.",
+ ["close"] = "If true, the session will be closed.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ // Submit via GET - v1 legacy
+ endpoints.MapGet("api/v1/events/submit", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync(new SubmitEventByGet(null, 1, null, httpContext.Request.GetClientUserAgent(), httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .WithTags("Event")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithMetadata(new ObsoleteAttribute("Use GET /api/v2/events/submit"))
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.SubmitGetAdditionalParameters,
+ ResponseDescriptions = new() {
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ endpoints.MapGet("api/v1/events/submit/{type:minlength(1)}", async (string type, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync(new SubmitEventByGet(null, 1, type, httpContext.Request.GetClientUserAgent(), httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .WithTags("Event")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithMetadata(new ObsoleteAttribute("Use GET /api/v2/events/submit"))
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.SubmitGetAdditionalParameters,
+ ResponseDescriptions = new() {
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ endpoints.MapGet("api/v1/projects/{projectId:objectid}/events/submit", async (string projectId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync(new SubmitEventByGet(projectId, 1, null, httpContext.Request.GetClientUserAgent(), httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .WithTags("Event")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithMetadata(new ObsoleteAttribute("Use GET /api/v2/events/submit"))
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.SubmitGetAdditionalParameters,
+ ResponseDescriptions = new() {
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ endpoints.MapGet("api/v1/projects/{projectId:objectid}/events/submit/{type:minlength(1)}", async (string projectId, string type, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync(new SubmitEventByGet(projectId, 1, type, httpContext.Request.GetClientUserAgent(), httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .WithTags("Event")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithMetadata(new ObsoleteAttribute("Use GET /api/v2/events/submit"))
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.SubmitGetAdditionalParameters,
+ ResponseDescriptions = new() {
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ // Submit via GET - v2
+ group.MapGet("events/submit", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? type = null)
+ => (await mediator.InvokeAsync(new SubmitEventByGet(null, 2, type, httpContext.Request.GetClientUserAgent(), httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Submit event by GET")
+ .WithDescription("""
+ You can submit an event using an HTTP GET and query string parameters. Any unknown query string parameters will be added to the extended data of the event.
+
+ Feature usage named build with a duration of 10:
+ ```/events/submit?access_token=YOUR_API_KEY&type=usage&source=build&value=10```
+
+ Log with message, geo and extended data
+ ```/events/submit?access_token=YOUR_API_KEY&type=log&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```
+ """)
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.SubmitGetAdditionalParameters,
+ ParameterDescriptions = new() {
+ ["type"] = "The event type (ie. error, log message, feature usage).",
+ ["source"] = "The event source (ie. machine name, log name, feature name).",
+ ["message"] = "The event message.",
+ ["reference"] = "An optional identifier to be used for referencing this event instance at a later time.",
+ ["date"] = "The date that the event occurred on.",
+ ["count"] = "The number of duplicated events.",
+ ["value"] = "The value of the event if any.",
+ ["geo"] = "The geo coordinates where the event happened.",
+ ["tags"] = "A list of tags used to categorize this event (comma separated).",
+ ["identity"] = "The user's identity that the event happened to.",
+ ["identityname"] = "The user's friendly name that the event happened to.",
+ ["userAgent"] = "The user agent that submitted the event.",
+ ["parameters"] = "Query string parameters that control what properties are set on the event",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ group.MapGet("events/submit/{type:minlength(1)}", async (string type, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync(new SubmitEventByGet(null, 2, type, httpContext.Request.GetClientUserAgent(), httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Submit event type by GET")
+ .WithDescription("""
+ You can submit an event using an HTTP GET and query string parameters.
+
+ Feature usage event named build with a value of 10:
+ ```/events/submit/usage?access_token=YOUR_API_KEY&source=build&value=10```
+
+ Log event with message, geo and extended data
+ ```/events/submit/log?access_token=YOUR_API_KEY&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```
+ """)
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.SubmitGetAdditionalParameters,
+ ParameterDescriptions = new() {
+ ["type"] = "The event type (ie. error, log message, feature usage).",
+ ["source"] = "The event source (ie. machine name, log name, feature name).",
+ ["message"] = "The event message.",
+ ["reference"] = "An optional identifier to be used for referencing this event instance at a later time.",
+ ["date"] = "The date that the event occurred on.",
+ ["count"] = "The number of duplicated events.",
+ ["value"] = "The value of the event if any.",
+ ["geo"] = "The geo coordinates where the event happened.",
+ ["tags"] = "A list of tags used to categorize this event (comma separated).",
+ ["identity"] = "The user's identity that the event happened to.",
+ ["identityname"] = "The user's friendly name that the event happened to.",
+ ["userAgent"] = "The user agent that submitted the event.",
+ ["parameters"] = "Query string parameters that control what properties are set on the event",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ group.MapGet("projects/{projectId:objectid}/events/submit", async (string projectId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? type = null)
+ => (await mediator.InvokeAsync(new SubmitEventByGet(projectId, 2, type, httpContext.Request.GetClientUserAgent(), httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Submit event type by GET for a specific project")
+ .WithDescription("You can submit an event using an HTTP GET and query string parameters.\n\nFeature usage named build with a duration of 10:\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=usage&source=build&value=10```\n\nLog with message, geo and extended data\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=log&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```")
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.SubmitGetAdditionalParameters,
+ ParameterDescriptions = new() {
+ ["projectId"] = "The identifier of the project.",
+ ["source"] = "The event source (ie. machine name, log name, feature name).",
+ ["message"] = "The event message.",
+ ["reference"] = "An optional identifier to be used for referencing this event instance at a later time.",
+ ["date"] = "The date that the event occurred on.",
+ ["count"] = "The number of duplicated events.",
+ ["value"] = "The value of the event if any.",
+ ["geo"] = "The geo coordinates where the event happened.",
+ ["tags"] = "A list of tags used to categorize this event (comma separated).",
+ ["identity"] = "The user's identity that the event happened to.",
+ ["identityname"] = "The user's friendly name that the event happened to.",
+ ["userAgent"] = "The user agent that submitted the event.",
+ ["parameters"] = "Query String parameters that control what properties are set on the event",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ group.MapGet("projects/{projectId:objectid}/events/submit/{type:minlength(1)}", async (string projectId, string type, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync(new SubmitEventByGet(projectId, 2, type, httpContext.Request.GetClientUserAgent(), httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Submit event type by GET for a specific project")
+ .WithDescription("You can submit an event using an HTTP GET and query string parameters.\n\nFeature usage named build with a duration of 10:\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=usage&source=build&value=10```\n\nLog with message, geo and extended data\n```/projects/{projectId}/events/submit?access_token=YOUR_API_KEY&type=log&message=Hello World&source=server01&geo=32.85,-96.9613&randomproperty=true```")
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.SubmitGetAdditionalParameters,
+ ParameterDescriptions = new() {
+ ["projectId"] = "The identifier of the project.",
+ ["type"] = "The event type (ie. error, log message, feature usage).",
+ ["source"] = "The event source (ie. machine name, log name, feature name).",
+ ["message"] = "The event message.",
+ ["reference"] = "An optional identifier to be used for referencing this event instance at a later time.",
+ ["date"] = "The date that the event occurred on.",
+ ["count"] = "The number of duplicated events.",
+ ["value"] = "The value of the event if any.",
+ ["geo"] = "The geo coordinates where the event happened.",
+ ["tags"] = "A list of tags used to categorize this event (comma separated).",
+ ["identity"] = "The user's identity that the event happened to.",
+ ["identityname"] = "The user's friendly name that the event happened to.",
+ ["userAgent"] = "The user agent that submitted the event.",
+ ["parameters"] = "Query String parameters that control what properties are set on the event",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ // Submit via POST - v1 legacy
+ endpoints.MapPost("api/v1/error", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => await SubmitEventByPostAsync(null, 1, httpContext, mediator, resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .WithTags("Event")
+ .Produces(StatusCodes.Status200OK)
+ .Produces(StatusCodes.Status202Accepted)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithMetadata(new ObsoleteAttribute("Use POST /api/v2/events"))
+ .WithMetadata(new RequestBodyContentAttribute("application/json", "text/plain"))
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.PostUserAgentParameter,
+ ResponseDescriptions = new() {
+ ["202"] = "Accepted",
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ endpoints.MapPost("api/v1/events", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => await SubmitEventByPostAsync(null, 1, httpContext, mediator, resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .WithTags("Event")
+ .Produces(StatusCodes.Status202Accepted)
+ .Produces(StatusCodes.Status413RequestEntityTooLarge)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithMetadata(new ObsoleteAttribute("Use POST /api/v2/events"))
+ .WithMetadata(new RequestBodyContentAttribute("application/json", "text/plain"))
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.PostUserAgentParameter,
+ ResponseDescriptions = new() {
+ ["202"] = "Accepted",
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ endpoints.MapPost("api/v1/projects/{projectId:objectid}/events", async (string projectId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => await SubmitEventByPostAsync(projectId, 1, httpContext, mediator, resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .WithTags("Event")
+ .Produces(StatusCodes.Status202Accepted)
+ .Produces(StatusCodes.Status413RequestEntityTooLarge)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithMetadata(new ObsoleteAttribute("Use POST /api/v2/events"))
+ .WithMetadata(new RequestBodyContentAttribute("application/json", "text/plain"))
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.PostUserAgentParameter,
+ ResponseDescriptions = new() {
+ ["202"] = "Accepted",
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ // Submit via POST - v2
+ group.MapPost("events", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => await SubmitEventByPostAsync(null, 2, httpContext, mediator, resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .Produces(StatusCodes.Status202Accepted)
+ .Produces(StatusCodes.Status413RequestEntityTooLarge)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Submit event by POST")
+ .WithDescription("""
+ You can create an event by posting any uncompressed or compressed (gzip or deflate) string or json object. If we know how to handle it we will create a new event. If none of the JSON properties match the event object then we will create a new event and place your JSON object into the events data collection.
+
+ You can also post a multi-line string. We automatically split strings by the \n character and create a new log event for every line.
+
+ Simple event:
+ ```{ "message": "Exceptionless is amazing!" }```
+
+ Simple log event with user identity:
+ ```{ "type": "log", "message": "Exceptionless is amazing!", "date":"2030-01-01T12:00:00.0000000-05:00", "@user":{ "identity":"123456789", "name": "Test User" } }```
+
+ Multiple events from string content:
+ ```Exceptionless is amazing!
+ Exceptionless is really amazing!```
+
+ Simple error:
+ ```{ "type": "error", "date":"2030-01-01T12:00:00.0000000-05:00", "@simple_error": { "message": "Simple Exception", "type": "System.Exception", "stack_trace": " at Client.Tests.ExceptionlessClientTests.CanSubmitSimpleException() in ExceptionlessClientTests.cs:line 77" } }```
+ """)
+ .WithMetadata(new RequestBodyContentAttribute("application/json", "text/plain"))
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.PostUserAgentParameter,
+ ParameterDescriptions = new() {
+ ["userAgent"] = "The user agent that submitted the event.",
+ },
+ ResponseDescriptions = new() {
+ ["202"] = "Accepted",
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ group.MapPost("projects/{projectId:objectid}/events", async (string projectId, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => await SubmitEventByPostAsync(projectId, 2, httpContext, mediator, resultMapper))
+ .RequireAuthorization(AuthorizationRoles.ClientPolicy)
+ .AddEndpointFilter()
+ .Produces(StatusCodes.Status202Accepted)
+ .Produces(StatusCodes.Status413RequestEntityTooLarge)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Submit event by POST for a specific project")
+ .WithDescription("""
+ You can create an event by posting any uncompressed or compressed (gzip or deflate) string or json object. If we know how to handle it we will create a new event. If none of the JSON properties match the event object then we will create a new event and place your JSON object into the events data collection.
+
+ You can also post a multi-line string. We automatically split strings by the \n character and create a new log event for every line.
+
+ Simple event:
+ ```{ "message": "Exceptionless is amazing!" }```
+
+ Simple log event with user identity:
+ ```{ "type": "log", "message": "Exceptionless is amazing!", "date":"2030-01-01T12:00:00.0000000-05:00", "@user":{ "identity":"123456789", "name": "Test User" } }```
+
+ Multiple events from string content:
+ ```Exceptionless is amazing!
+ Exceptionless is really amazing!```
+
+ Simple error:
+ ```{ "type": "error", "date":"2030-01-01T12:00:00.0000000-05:00", "@simple_error": { "message": "Simple Exception", "type": "System.Exception", "stack_trace": " at Client.Tests.ExceptionlessClientTests.CanSubmitSimpleException() in ExceptionlessClientTests.cs:line 77" } }```
+ """)
+ .WithMetadata(new RequestBodyContentAttribute("application/json", "text/plain"))
+ .WithMetadata(new EndpointDocumentation {
+ AdditionalParameters = EventEndpointHelpers.PostUserAgentParameter,
+ ParameterDescriptions = new() {
+ ["projectId"] = "The identifier of the project.",
+ ["userAgent"] = "The user agent that submitted the event.",
+ },
+ ResponseDescriptions = new() {
+ ["202"] = "Accepted",
+ ["400"] = "No project id specified and no default project was found.",
+ ["404"] = "No project was found.",
+ }
+ });
+
+ // Delete
+ group.MapDelete("events/{ids:objectids}", async (string ids, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync>(new DeleteEvents(ids, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.UserPolicy)
+ .Produces(StatusCodes.Status202Accepted)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status500InternalServerError)
+ .WithSummary("Remove")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["ids"] = "A comma-delimited list of event identifiers.",
+ },
+ ResponseDescriptions = new() {
+ ["202"] = "Accepted",
+ ["400"] = "One or more validation errors occurred.",
+ ["404"] = "One or more event occurrences were not found.",
+ ["500"] = "An error occurred while deleting one or more event occurrences.",
+ }
+ });
+
+ return endpoints;
+ }
+
+ private static async Task SubmitEventByPostAsync(string? projectId, int apiVersion, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ {
+ var contentTypeResult = ApiValidation.ValidateContentType(httpContext.Request, "application/json", "text/plain");
+ if (contentTypeResult is not null)
+ return contentTypeResult;
+
+ if (httpContext.Request.ContentLength is <= 0)
+ return Microsoft.AspNetCore.Http.Results.StatusCode(StatusCodes.Status202Accepted);
+
+ return (await mediator.InvokeAsync(new SubmitEventByPost(projectId, apiVersion, httpContext.Request.GetClientUserAgent(), httpContext))).ToHttpResult(resultMapper);
+ }
+}
+
+internal static class EventEndpointHelpers
+{
+ ///
+ /// Additional parameters for all event submit GET endpoints.
+ /// These are read from HttpContext/query string rather than method parameters.
+ ///
+ public static readonly List SubmitGetAdditionalParameters =
+ [
+ new("source", "query", Description: "The event source (ie. machine name, log name, feature name)."),
+ new("message", "query", Description: "The event message."),
+ new("reference", "query", Description: "An optional identifier to be used for referencing this event instance at a later time."),
+ new("date", "query", Description: "The date that the event occurred on."),
+ new("count", "query", Description: "The number of duplicated events.", Type: "integer", Format: "int32"),
+ new("value", "query", Description: "The value of the event if any.", Type: "number", Format: "double"),
+ new("geo", "query", Description: "The geo coordinates where the event happened."),
+ new("tags", "query", Description: "A list of tags used to categorize this event (comma separated)."),
+ new("identity", "query", Description: "The user's identity that the event happened to."),
+ new("identityname", "query", Description: "The user's friendly name that the event happened to."),
+ new("userAgent", "header", Description: "The user agent that submitted the event."),
+ new("parameters", "query", Description: "Query string parameters that control what properties are set on the event", Type: "array"),
+ ];
+
+ ///
+ /// Additional parameters for POST event endpoints (just userAgent header).
+ ///
+ public static readonly List PostUserAgentParameter =
+ [
+ new("userAgent", "header", Description: "The user agent that submitted the event."),
+ ];
+}
diff --git a/src/Exceptionless.Web/Api/Endpoints/OAuthApplicationEndpoints.cs b/src/Exceptionless.Web/Api/Endpoints/OAuthApplicationEndpoints.cs
new file mode 100644
index 0000000000..d58e09e062
--- /dev/null
+++ b/src/Exceptionless.Web/Api/Endpoints/OAuthApplicationEndpoints.cs
@@ -0,0 +1,48 @@
+using Exceptionless.Core.Authorization;
+using Exceptionless.Web.Api.Filters;
+using Exceptionless.Web.Api.Messages;
+using Exceptionless.Web.Api.Results;
+using Exceptionless.Web.Models.Admin;
+using Foundatio.Mediator;
+using HttpIResult = Microsoft.AspNetCore.Http.IResult;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Exceptionless.Web.Api.Endpoints;
+
+public static class OAuthApplicationEndpoints
+{
+ public static IEndpointRouteBuilder MapOAuthApplicationEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ var group = endpoints.MapGroup("api/v2/admin/oauth-applications")
+ .RequireAuthorization(AuthorizationRoles.GlobalAdminPolicy)
+ .AddEndpointFilter()
+ .ExcludeFromDescription();
+
+ endpoints.MapGet("api/v2/admin/oauth-applications", async (IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync>>(new GetOAuthApplications())).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.GlobalAdminPolicy)
+ .AddEndpointFilter()
+ .ExcludeFromDescription();
+
+ endpoints.MapPost("api/v2/admin/oauth-applications", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, [FromBody] NewOAuthApplication model)
+ => (await mediator.InvokeAsync>(new CreateOAuthApplicationMessage(model, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.GlobalAdminPolicy)
+ .AddEndpointFilter()
+ .ExcludeFromDescription()
+ .Produces(StatusCodes.Status201Created)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity);
+
+ group.MapPut("{id:objectid}", async (string id, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, [FromBody] UpdateOAuthApplication model)
+ => (await mediator.InvokeAsync>(new UpdateOAuthApplicationMessage(id, model, httpContext))).ToHttpResult(resultMapper))
+ .Produces()
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity);
+
+ group.MapDelete("{id:objectid}", async (string id, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync(new DeleteOAuthApplicationMessage(id))).ToHttpResult(resultMapper))
+ .Produces(StatusCodes.Status204NoContent)
+ .ProducesProblem(StatusCodes.Status404NotFound);
+
+ return endpoints;
+ }
+}
diff --git a/src/Exceptionless.Web/Api/Endpoints/OAuthEndpoints.cs b/src/Exceptionless.Web/Api/Endpoints/OAuthEndpoints.cs
new file mode 100644
index 0000000000..bae765585e
--- /dev/null
+++ b/src/Exceptionless.Web/Api/Endpoints/OAuthEndpoints.cs
@@ -0,0 +1,122 @@
+using Exceptionless.Core.Authorization;
+using Exceptionless.Core.Services;
+using Exceptionless.Web.Api.Messages;
+using Exceptionless.Web.Models.OAuth;
+using Foundatio.Mediator;
+using HttpIResult = Microsoft.AspNetCore.Http.IResult;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Exceptionless.Web.Api.Endpoints;
+
+public static class OAuthEndpoints
+{
+ public static IEndpointRouteBuilder MapOAuthEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ endpoints.MapGet(".well-known/oauth-authorization-server", async (IMediator mediator)
+ => await mediator.InvokeAsync(new GetAuthorizationServerMetadata()))
+ .AllowAnonymous()
+ .WithTags("OAuth")
+ .Produces();
+
+ endpoints.MapGet(".well-known/oauth-protected-resource/mcp", async (IMediator mediator)
+ => await mediator.InvokeAsync(new GetMcpProtectedResourceMetadata()))
+ .AllowAnonymous()
+ .WithTags("OAuth")
+ .Produces();
+
+ endpoints.MapGet(".well-known/oauth-protected-resource/api/v2", async (IMediator mediator)
+ => await mediator.InvokeAsync(new GetRestApiProtectedResourceMetadata()))
+ .AllowAnonymous()
+ .WithTags("OAuth")
+ .Produces();
+
+ var group = endpoints.MapGroup("api/v2/oauth")
+ .WithTags("OAuth");
+
+ group.MapGet("authorize", async (
+ IMediator mediator,
+ [FromQuery(Name = "client_id")] string? clientId = null,
+ [FromQuery(Name = "response_type")] string? responseType = null,
+ [FromQuery(Name = "redirect_uri")] string? redirectUri = null,
+ [FromQuery] string? scope = null,
+ [FromQuery] string? state = null,
+ [FromQuery(Name = "code_challenge")] string? codeChallenge = null,
+ [FromQuery(Name = "code_challenge_method")] string? codeChallengeMethod = null,
+ [FromQuery] string? resource = null)
+ => await mediator.InvokeAsync(new RedirectToAuthorizeBridge()))
+ .AllowAnonymous()
+ .Produces(StatusCodes.Status302Found);
+
+ group.MapPost("authorize", async (IMediator mediator, [FromBody] OAuthAuthorizeForm form)
+ => await mediator.InvokeAsync(new CompleteOAuthAuthorization(form)))
+ .RequireAuthorization(AuthorizationRoles.UserPolicy)
+ .Accepts("application/json", "application/*+json", "application/octet-stream", "text/json", "text/plain")
+ .Produces()
+ .Produces(StatusCodes.Status400BadRequest);
+
+ group.MapPost("authorize/consent", async (IMediator mediator, [FromBody] OAuthAuthorizeForm form)
+ => await mediator.InvokeAsync(new GetOAuthAuthorizeConsent(form)))
+ .RequireAuthorization(AuthorizationRoles.UserPolicy)
+ .Accepts("application/json", "application/*+json", "application/octet-stream", "text/json", "text/plain")
+ .Produces()
+ .Produces(StatusCodes.Status400BadRequest);
+
+ group.MapPost("register", async (IMediator mediator, [FromBody] OAuthClientRegistrationRequest request)
+ => await mediator.InvokeAsync(new RegisterOAuthClient(request)))
+ .AllowAnonymous()
+ .Accepts("application/json", "application/*+json", "application/octet-stream", "text/json", "text/plain")
+ .Produces(StatusCodes.Status201Created)
+ .Produces(StatusCodes.Status400BadRequest)
+ .Produces(StatusCodes.Status429TooManyRequests);
+
+ group.MapPost("token", IssueTokenAsync)
+ .AllowAnonymous()
+ .Accepts("application/x-www-form-urlencoded")
+ .Produces()
+ .Produces(StatusCodes.Status400BadRequest)
+ .DisableAntiforgery();
+
+ group.MapPost("revoke", RevokeTokenAsync)
+ .AllowAnonymous()
+ .Accepts("application/x-www-form-urlencoded")
+ .Produces(StatusCodes.Status200OK)
+ .DisableAntiforgery();
+
+ return endpoints;
+ }
+
+ private static async Task IssueTokenAsync(IMediator mediator, HttpRequest request, CancellationToken cancellationToken)
+ {
+ var form = await request.ReadFormAsync(cancellationToken);
+ var tokenForm = new OAuthTokenForm
+ {
+ GrantType = GetFormValue(form, "grant_type") ?? String.Empty,
+ Code = GetFormValue(form, "code"),
+ RedirectUri = GetFormValue(form, "redirect_uri"),
+ ClientId = GetFormValue(form, "client_id"),
+ CodeVerifier = GetFormValue(form, "code_verifier"),
+ RefreshToken = GetFormValue(form, "refresh_token"),
+ Resource = GetFormValue(form, "resource")
+ };
+
+ return await mediator.InvokeAsync(new IssueOAuthToken(tokenForm));
+ }
+
+ private static async Task RevokeTokenAsync(IMediator mediator, HttpRequest request, CancellationToken cancellationToken)
+ {
+ var form = await request.ReadFormAsync(cancellationToken);
+ var revokeForm = new OAuthRevokeForm
+ {
+ Token = GetFormValue(form, "token"),
+ ClientId = GetFormValue(form, "client_id")
+ };
+
+ return await mediator.InvokeAsync(new RevokeOAuthToken(revokeForm));
+ }
+
+ private static string? GetFormValue(IFormCollection form, string key)
+ {
+ string? value = form[key].FirstOrDefault();
+ return String.IsNullOrWhiteSpace(value) ? null : value;
+ }
+}
diff --git a/src/Exceptionless.Web/Api/Endpoints/OrganizationEndpoints.cs b/src/Exceptionless.Web/Api/Endpoints/OrganizationEndpoints.cs
new file mode 100644
index 0000000000..163bcb9681
--- /dev/null
+++ b/src/Exceptionless.Web/Api/Endpoints/OrganizationEndpoints.cs
@@ -0,0 +1,468 @@
+using Exceptionless.Core.Authorization;
+using Exceptionless.Core.Extensions;
+using Exceptionless.Core.Models;
+using Exceptionless.Core.Models.Billing;
+using Exceptionless.Core.Repositories;
+using Exceptionless.Web.Api.Filters;
+using Exceptionless.Web.Api.Infrastructure;
+using Exceptionless.Web.Api.Results;
+using Exceptionless.Web.Controllers;
+using Exceptionless.Web.Models;
+using Exceptionless.Web.Utility;
+using Foundatio.Storage;
+using Foundatio.Mediator;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using OrganizationMessages = Exceptionless.Web.Api.Messages;
+using Invoice = Exceptionless.Web.Models.Invoice;
+using Exceptionless.Web.Utility.OpenApi;
+using HttpIResult = Microsoft.AspNetCore.Http.IResult;
+using HttpResults = Microsoft.AspNetCore.Http.Results;
+
+namespace Exceptionless.Web.Api.Endpoints;
+
+public static class OrganizationEndpoints
+{
+ public static IEndpointRouteBuilder MapOrganizationEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ var group = endpoints.MapGroup("api/v2")
+ .RequireAuthorization(AuthorizationRoles.UserPolicy)
+ .AddEndpointFilter()
+ .WithTags("Organization");
+
+ group.MapGet("organizations", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? filter = null, string? mode = null)
+ => (await mediator.InvokeAsync>>(new OrganizationMessages.GetOrganizations(filter, mode, httpContext))).ToHttpResult(resultMapper))
+ .Produces>()
+ .WithSummary("Get all")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["filter"] = "A filter that controls what data is returned from the server.",
+ ["mode"] = "If no mode is set then a lightweight organization object will be returned. If the mode is set to stats than the fully populated object will be returned.",
+ }
+ });
+
+ group.MapGet("admin/organizations", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? criteria = null, bool? paid = null, bool? suspended = null, string? mode = null, int page = 1, int limit = 10, OrganizationSortBy sort = OrganizationSortBy.Newest)
+ => (await mediator.InvokeAsync>>(new OrganizationMessages.GetAdminOrganizations(criteria, paid, suspended, mode, page, limit, sort, httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.GlobalAdminPolicy)
+ .Produces>()
+ .ExcludeFromDescription();
+
+ group.MapGet("admin/organizations/stats", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync>(new OrganizationMessages.GetOrganizationPlanStats(httpContext))).ToHttpResult(resultMapper))
+ .RequireAuthorization(AuthorizationRoles.GlobalAdminPolicy)
+ .Produces()
+ .ExcludeFromDescription();
+
+ group.MapGet("organizations/{id:objectid}", async (string id, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? mode = null)
+ => (await mediator.InvokeAsync>(new OrganizationMessages.GetOrganizationById(id, mode, httpContext))).ToHttpResult(resultMapper))
+ .WithName("GetOrganizationById")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Get by id")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the organization.",
+ ["mode"] = "If no mode is set then the a lightweight organization object will be returned. If the mode is set to stats than the fully populated object will be returned.",
+ },
+ ResponseDescriptions = new() {
+ ["404"] = "The organization could not be found.",
+ }
+ });
+
+ group.MapPost("organizations", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, [FromBody] NewOrganization organization) =>
+ {
+ return (await mediator.InvokeAsync>(new OrganizationMessages.CreateOrganization(organization, httpContext))).ToHttpResult(resultMapper);
+ })
+ .Accepts("application/json", "application/*+json")
+ .Produces(StatusCodes.Status201Created)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status409Conflict)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Create")
+ .WithMetadata(new EndpointDocumentation {
+ RequestBodyDescription = "The organization.",
+ ResponseDescriptions = new() {
+ ["201"] = "Created",
+ ["400"] = "An error occurred while creating the organization.",
+ ["409"] = "The organization already exists.",
+ }
+ });
+
+ group.MapPatch("organizations/{id:objectid}", async (string id, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, [FromBody] Delta? changes) =>
+ {
+ if (changes is null)
+ return ApiValidation.MissingRequestBody();
+
+ return (await mediator.InvokeAsync>(new OrganizationMessages.UpdateOrganizationMessage(id, changes, httpContext))).ToHttpResult(resultMapper);
+ })
+ .Accepts>("application/json", "application/*+json")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Update")
+ .WithMetadata(new EndpointDocumentation {
+ RequestBodyRequired = true,
+ RequestBodyDescription = "The changes",
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the organization.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "An error occurred while updating the organization.",
+ ["404"] = "The organization could not be found.",
+ }
+ });
+
+ group.MapPut("organizations/{id:objectid}", async (string id, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, [FromBody] Delta? changes) =>
+ {
+ if (changes is null)
+ return ApiValidation.MissingRequestBody();
+
+ return (await mediator.InvokeAsync>(new OrganizationMessages.UpdateOrganizationMessage(id, changes, httpContext))).ToHttpResult(resultMapper);
+ })
+ .Accepts>("application/json", "application/*+json")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Update")
+ .WithMetadata(new EndpointDocumentation {
+ RequestBodyRequired = true,
+ RequestBodyDescription = "The changes",
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the organization.",
+ },
+ ResponseDescriptions = new() {
+ ["400"] = "An error occurred while updating the organization.",
+ ["404"] = "The organization could not be found.",
+ }
+ });
+
+ group.MapPost("organizations/{id:objectid}/icon", UploadIconAsync)
+ .Accepts("multipart/form-data")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithMetadata(
+ new MultipartFileUploadAttribute(),
+ new RequestSizeLimitAttribute(ProfileImageStorage.MaxRequestBodySize),
+ new RequestFormLimitsAttribute { MultipartBodyLengthLimit = ProfileImageStorage.MaxRequestBodySize })
+ .WithSummary("Upload icon")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the organization.",
+ },
+ ResponseDescriptions = new() {
+ ["404"] = "The organization could not be found.",
+ ["422"] = "The image file is invalid.",
+ }
+ })
+ .DisableAntiforgery();
+
+ group.MapDelete("organizations/{id:objectid}/icon", DeleteIconAsync)
+ .Produces()
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Remove icon")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the organization.",
+ },
+ ResponseDescriptions = new() {
+ ["404"] = "The organization could not be found.",
+ }
+ });
+
+ group.MapGet("organizations/{id:objectid}/icon/{fileName}", GetIconAsync)
+ .AllowAnonymous()
+ .WithName("GetOrganizationIcon")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Get icon")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the organization.",
+ ["fileName"] = "The icon file name.",
+ },
+ ResponseDescriptions = new() {
+ ["404"] = "The icon could not be found.",
+ }
+ });
+
+ group.MapDelete("organizations/{ids:objectids}", async (string ids, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync>(new OrganizationMessages.DeleteOrganizations(ids.FromDelimitedString(), httpContext))).ToHttpResult(resultMapper))
+ .Produces(StatusCodes.Status202Accepted)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status500InternalServerError)
+ .WithSummary("Remove")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["ids"] = "A comma-delimited list of organization identifiers.",
+ },
+ ResponseDescriptions = new() {
+ ["202"] = "Accepted",
+ ["400"] = "One or more validation errors occurred.",
+ ["404"] = "One or more organizations were not found.",
+ ["500"] = "An error occurred while deleting one or more organizations.",
+ }
+ });
+
+ group.MapGet("organizations/invoice/{id:minlength(10)}", async (string id, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync>(new OrganizationMessages.GetInvoice(id, httpContext))).ToHttpResult(resultMapper))
+ .Produces()
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Get invoice")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the invoice.",
+ },
+ ResponseDescriptions = new() {
+ ["404"] = "The invoice was not found.",
+ }
+ });
+
+ group.MapGet("organizations/{id:objectid}/invoices", async (string id, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper, string? before = null, string? after = null, int limit = 12)
+ => (await mediator.InvokeAsync>>(new OrganizationMessages.GetInvoices(id, before, after, limit, httpContext))).ToHttpResult(resultMapper))
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Get invoices")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the organization.",
+ ["before"] = "A cursor for use in pagination. before is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with obj_bar, your subsequent call can include before=obj_bar in order to fetch the previous page of the list.",
+ ["after"] = "A cursor for use in pagination. after is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.",
+ ["limit"] = "A limit on the number of objects to be returned. Limit can range between 1 and 100 items.",
+ },
+ ResponseDescriptions = new() {
+ ["404"] = "The organization was not found.",
+ }
+ });
+
+ group.MapGet("organizations/{id:objectid}/plans", async (string id, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper)
+ => (await mediator.InvokeAsync>>(new OrganizationMessages.GetPlans(id, httpContext))).ToHttpResult(resultMapper))
+ .Produces>()
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Get plans")
+ .WithDescription("Gets available plans for a specific organization.")
+ .WithMetadata(new EndpointDocumentation {
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the organization.",
+ },
+ ResponseDescriptions = new() {
+ ["404"] = "The organization was not found.",
+ }
+ });
+
+ group.MapPost("organizations/{id:objectid}/change-plan", async (string id, HttpContext httpContext, IMediator mediator, IMediatorResultMapper resultMapper,
+ [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] ChangePlanRequest? model = null,
+ [FromQuery] string? planId = null,
+ [FromQuery] string? stripeToken = null,
+ [FromQuery] string? last4 = null,
+ [FromQuery] string? couponId = null)
+ => (await mediator.InvokeAsync>(new OrganizationMessages.ChangeOrganizationPlan(id, model, planId, stripeToken, last4, couponId, httpContext))).ToHttpResult(resultMapper))
+ .Accepts(true, "application/json", "application/*+json", "application/octet-stream", "text/json", "text/plain")
+ .Produces()
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .ProducesProblem(StatusCodes.Status422UnprocessableEntity)
+ .WithSummary("Change plan")
+ .WithDescription("Upgrades or downgrades the organization's plan. Accepts parameters via JSON body (preferred) or query string (legacy).")
+ .WithMetadata(new EndpointDocumentation {
+ RequestBodyDescription = "The plan change request (JSON body).",
+ ParameterDescriptions = new() {
+ ["id"] = "The identifier of the organization.",
+ ["planId"] = "Legacy query parameter: the plan identifier.",
+ ["stripeToken"] = "Legacy query parameter: the Stripe token.",
+ ["last4"] = "Legacy query parameter: last four digits of the card.",
+ ["couponId"] = "Legacy query parameter: the coupon identifier.",
+ },
+ ResponseDescriptions = new() {
+ ["404"] = "The organization was not found.",
+ }
+ });
+
+ group.MapPost("organizations/{id:objectid}/users/{email:minlength(1)}", async (string id, string email, HttpContext httpContext, IMediator mediator, IMediatorResultMapper