Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
98d9988
Add OpenSpec change: minimal-api-mediator-openapi-migration
niemyjski May 26, 2026
36cd1bc
refactor: consolidate Startup.cs into minimal hosting Program.cs
niemyjski May 26, 2026
c22ccaa
feat: add Api infrastructure (endpoints, groups, results, filters)
niemyjski May 26, 2026
9312c10
feat: migrate StatusEndpoints and UtilityEndpoints to Minimal API
niemyjski May 26, 2026
5e2b597
feat: migrate Token, WebHook, and Stripe endpoints to Minimal API
niemyjski May 26, 2026
364841b
Migrate SavedViewController and UserController to Minimal API endpoin…
niemyjski May 26, 2026
17c5167
feat: migrate Project and Organization endpoints to Minimal API
niemyjski May 27, 2026
539e61d
feat: migrate Auth endpoints to Minimal API
niemyjski May 27, 2026
cbe2ac5
Migrate Stack, Admin, Event controllers to Minimal API endpoints
niemyjski May 27, 2026
31643d3
Fix minimal API parity regressions
niemyjski May 27, 2026
4f00649
refactor: remove MVC controllers infrastructure
niemyjski May 27, 2026
b1dc7b6
test: add route manifest and OpenAPI snapshot tests
niemyjski May 27, 2026
a68fbe7
fix: address audit findings — route constraints, validation filter, w…
niemyjski May 27, 2026
a2ae9a2
fix: regenerate endpoint-manifest.json with corrected routes
niemyjski May 27, 2026
730e050
refactor: modernize Job runner to minimal hosting pattern
niemyjski May 27, 2026
1356c67
docs: restore API documentation metadata on all endpoints
niemyjski May 27, 2026
5be3aaf
docs: add missing OpenAPI response codes, parameters, and schema types
niemyjski May 27, 2026
69255d5
fix: restore original OpenAPI tags to match controller-derived tag names
niemyjski May 27, 2026
7b403d5
fix: address PR feedback and CI build failure
niemyjski May 27, 2026
30aed31
fix: disable ValidateOnBuild in test factory for CI
niemyjski May 27, 2026
8c5fcdf
fix: resolve Program type ambiguity and address CodeQL feedback
niemyjski May 27, 2026
6bb1dd4
fix: correct ConfigurationResponseEndpointFilter status code check an…
niemyjski May 27, 2026
07ce2c9
fix: resolve validation parity, security bug, and paging issues
niemyjski May 27, 2026
859addf
fix: resolve flaky CI tests and restore OpenAPI schema parity
niemyjski May 27, 2026
5cf1cac
fix: correct stack endpoint response code metadata
niemyjski May 27, 2026
0d370c3
fix: serialize queue-dependent tests to prevent parallel interference
niemyjski May 27, 2026
06a9a9a
fix: restore OpenAPI schema parity with snake_case naming and documen…
niemyjski May 27, 2026
07838fb
fix(security): add organization access check to OrganizationHandler
niemyjski May 27, 2026
55c268d
feat: migrate TokenHandler to Result<T> return types
niemyjski May 28, 2026
025db45
feat: migrate Stack, Auth, Event, Stripe handlers to Result<T>
niemyjski May 28, 2026
3c88785
chore: remove accidental audit-output files
niemyjski May 28, 2026
57e339e
feat: migrate WebHook, Admin, User, SavedView, Project, Organization …
niemyjski May 28, 2026
2a49058
fix: correct status code mappings to preserve original behavior
niemyjski May 28, 2026
701f9ba
chore: gitignore audit-output directory
niemyjski May 28, 2026
aa9a681
merge: resolve conflict in SavedViewHandler after main merge
niemyjski May 28, 2026
61e141a
fix: address PR code quality feedback
niemyjski May 28, 2026
ae90a8f
fix: correct ModelActionResults status code and result mapper parity
niemyjski May 28, 2026
e69d7ee
fix: preserve per-ID failure details in bulk-delete responses
niemyjski May 28, 2026
79ecb52
docs: update backend-architecture skill with Minimal API + Mediator p…
niemyjski May 28, 2026
7a03dcf
fix: address code quality review findings
niemyjski May 28, 2026
43cceec
fix: address adversarial review findings
niemyjski May 28, 2026
9b70370
merge: port origin/main changes to minimal API handlers
niemyjski May 30, 2026
6d9d3f9
fix: add missing publish parameter to RemoveSystemNotification
niemyjski May 30, 2026
4e57a6b
fix: restore correct 426 status for suspended org reads and structure…
niemyjski May 31, 2026
2b58a7d
feat: replace Delta<T> with RFC 6902 JSON Patch support
niemyjski May 31, 2026
11bef8d
fix: set application/json-patch+json content-type in both frontends a…
niemyjski May 31, 2026
a3efdc4
style: fix ESLint double-quote and Prettier formatting in frontend PA…
niemyjski May 31, 2026
fc58321
fix: correct import ordering for eslint perfectionist/sort-imports
niemyjski May 31, 2026
944a219
style: fix remaining single-quote ESLint errors in Angular toJsonPatch
niemyjski May 31, 2026
38b4d7a
fix: add DefaultJsonTypeInfoResolver to test options for Release mode…
niemyjski May 31, 2026
5d912bd
Merge remote-tracking branch 'origin/main' into niemyjski/minimal-api…
niemyjski May 31, 2026
22cd049
fix: reject malformed json patch paths and restore event openapi bodies
niemyjski May 31, 2026
694fe45
fix: isolate test file storage by app scope
niemyjski May 31, 2026
f8806ba
Merge remote-tracking branch 'origin/main' into niemyjski/minimal-api…
ejsmith Jun 14, 2026
37a2285
Use mediator result mapper for minimal API responses
ejsmith Jun 14, 2026
28d138a
Remove legacy direct result mapping overloads
ejsmith Jun 14, 2026
3bf24dd
Pilot mediator generated endpoints for status routes
ejsmith Jun 14, 2026
67da4d0
Convert simple admin routes to mediator endpoints
ejsmith Jun 15, 2026
691c46f
Chain mediator result mapping from AddMediator
ejsmith Jun 15, 2026
89a1b4d
Use mediator 1.3.1 endpoint groups
ejsmith Jun 15, 2026
06b54cd
Merge remote-tracking branch 'origin/main' into feature/pr-2257-minim…
niemyjski Jun 18, 2026
fd25bdf
fix: address PR review feedback
niemyjski Jun 18, 2026
8f5dde3
Merge remote-tracking branch 'origin/main' into pr-2257
ejsmith Jun 22, 2026
57f0363
Fix event include total merge fallout
ejsmith Jun 22, 2026
d16aa20
Relax Elasticsearch test startup wait
ejsmith Jun 22, 2026
be5f960
Merge remote-tracking branch 'origin/main' into pr-2257
ejsmith Jun 22, 2026
159c257
Merge remote-tracking branch 'origin/main' into pr-2257
ejsmith Jun 24, 2026
9b16fa9
Fix cursor pagination next links
ejsmith Jun 24, 2026
113c44b
Preserve legacy partial update bodies
ejsmith Jun 25, 2026
8d4f2f5
Address code quality review feedback
ejsmith Jun 25, 2026
531b506
Refine webhook access filtering
ejsmith Jun 25, 2026
70dcb7f
Merge origin/main into minimal API migration
niemyjski Jul 1, 2026
812d160
Merge remote-tracking branch 'origin/main' into feature/minimal-api-m…
niemyjski Jul 6, 2026
6b0c071
Merge remote-tracking branch 'origin/main' into niemyjski/minimal-api…
niemyjski Jul 7, 2026
9808110
docs: add minimal api audit openspec delta
niemyjski Jul 7, 2026
6d80b5d
Merge remote-tracking branch 'origin/main' into niemyjski/minimal-api…
niemyjski Jul 9, 2026
35c64d2
fix: preserve webhook validation contract
niemyjski Jul 9, 2026
5c7fee0
fix: preserve patch and UI form compatibility
niemyjski Jul 9, 2026
6efcd2b
fix: stabilize saved view and webhook actions
niemyjski Jul 10, 2026
59faefd
Merge remote-tracking branch 'origin/main' into niemyjski/minimal-api…
niemyjski Jul 10, 2026
bf39fad
chore: remove migration audit artifacts
niemyjski Jul 10, 2026
bd23f65
Revert "refactor: modernize Job runner to minimal hosting pattern"
niemyjski Jul 10, 2026
dd06d7e
Revert "docs: update backend-architecture skill with Minimal API + Me…
niemyjski Jul 10, 2026
33cc8d8
revert: remove unrelated frontend changes
niemyjski Jul 10, 2026
50ac89a
Revert "fix: preserve patch and UI form compatibility"
niemyjski Jul 10, 2026
255086a
Revert "Preserve legacy partial update bodies"
niemyjski Jul 10, 2026
7be4ede
Revert "fix: reject malformed json patch paths and restore event open…
niemyjski Jul 10, 2026
33660b2
Revert "fix: add DefaultJsonTypeInfoResolver to test options for Rele…
niemyjski Jul 10, 2026
b942ea2
Revert "fix: set application/json-patch+json content-type in both fro…
niemyjski Jul 10, 2026
3fc9acb
Revert "feat: replace Delta<T> with RFC 6902 JSON Patch support"
niemyjski Jul 10, 2026
7b942ac
refactor: narrow minimal API migration scope
niemyjski Jul 10, 2026
907c33e
Merge remote-tracking branch 'origin/main' into feature/pr-2257-audit
niemyjski Jul 10, 2026
28ca19d
refactor: restore generated mediator endpoints
niemyjski Jul 10, 2026
3e31969
fix: complete minimal API parity audit
niemyjski Jul 10, 2026
6d6b751
fix: preserve remaining endpoint contracts
niemyjski Jul 11, 2026
526f8df
fix: minimize OpenAPI contract drift
niemyjski Jul 11, 2026
0f1a41d
refactor: generalize OpenAPI schema documentation
niemyjski Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ artifacts
[Bb]uild[Ll]og.*
coverage/
src/Exceptionless.Web/ClientApp/playwright-report/
audit-output/
src/Exceptionless.Web/ClientApp/test-results/

# IDE / editor
Expand Down
1 change: 0 additions & 1 deletion docs/_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ site.ignore(
"public",
"README.md",
"scripts",
"serialization-architecture.md",
"site-migration-plan.md",
"snapshots",
)
Expand Down
459 changes: 0 additions & 459 deletions docs/serialization-architecture.md

This file was deleted.

2 changes: 1 addition & 1 deletion src/Exceptionless.Job/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
{
Expand Down
30 changes: 30 additions & 0 deletions src/Exceptionless.Web/Api/ApiEndpoints.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
33 changes: 33 additions & 0 deletions src/Exceptionless.Web/Api/Endpoints/AdminEndpoints.cs
Original file line number Diff line number Diff line change
@@ -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<AutoValidationEndpointFilter>()
.ExcludeFromDescription();

group.MapGet("echo", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper<HttpIResult> resultMapper)
=> (await mediator.InvokeAsync<Result<object>>(new GetAdminEcho(httpContext))).ToHttpResult(resultMapper));

group.MapPost("change-plan", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper<HttpIResult> resultMapper, string organizationId, string planId)
=> (await mediator.InvokeAsync<Result<object>>(new AdminChangePlan(organizationId, planId, httpContext))).ToHttpResult(resultMapper));

group.MapPost("set-bonus", async (HttpContext httpContext, IMediator mediator, IMediatorResultMapper<HttpIResult> resultMapper, string organizationId, int bonusEvents, DateTime? expires = null)
=> (await mediator.InvokeAsync<Result>(new AdminSetBonus(organizationId, bonusEvents, expires, httpContext))).ToHttpResult(resultMapper));

group.MapPost("generate-sample-events", async (IMediator mediator, IMediatorResultMapper<HttpIResult> resultMapper, int eventCount = 250, int daysBack = 7)
=> (await mediator.InvokeAsync<Result<object>>(new AdminGenerateSampleEvents(eventCount, daysBack))).ToHttpResult(resultMapper));

return endpoints;
}
}
265 changes: 265 additions & 0 deletions src/Exceptionless.Web/Api/Endpoints/AuthEndpoints.cs
Original file line number Diff line number Diff line change
@@ -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<AutoValidationEndpointFilter>()
.WithTags("Auth");

group.MapPost("login", async (IMediator mediator, IMediatorResultMapper<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] Login model) =>
{
return (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.LoginMessage(model, httpContext))).ToHttpResult(resultMapper);
})
.AllowAnonymous()
.Accepts<Login>("application/json", "application/*+json")
.Produces<TokenResult>()
.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<HttpIResult> resultMapper, HttpContext httpContext)
=> (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.GetIntercomToken(httpContext))).ToHttpResult(resultMapper))
.Produces<TokenResult>()
.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<HttpIResult> resultMapper, HttpContext httpContext)
=> (await mediator.InvokeAsync<Result>(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<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] Signup model) =>
{
return (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.SignupMessage(model, httpContext))).ToHttpResult(resultMapper);
})
.AllowAnonymous()
.Accepts<Signup>("application/json", "application/*+json")
.Produces<TokenResult>()
.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<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>
{
return (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.GitHubLogin(value, httpContext))).ToHttpResult(resultMapper);
})
.AllowAnonymous()
.Accepts<ExternalAuthInfo>("application/json", "application/*+json")
.Produces<TokenResult>()
.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<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>
{
return (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.GoogleLogin(value, httpContext))).ToHttpResult(resultMapper);
})
.AllowAnonymous()
.Accepts<ExternalAuthInfo>("application/json", "application/*+json")
.Produces<TokenResult>()
.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<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>
{
return (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.FacebookLogin(value, httpContext))).ToHttpResult(resultMapper);
})
.AllowAnonymous()
.Accepts<ExternalAuthInfo>("application/json", "application/*+json")
.Produces<TokenResult>()
.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<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>
{
return (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.LiveLogin(value, httpContext))).ToHttpResult(resultMapper);
})
.AllowAnonymous()
.Accepts<ExternalAuthInfo>("application/json", "application/*+json")
.Produces<TokenResult>()
.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<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] ValueFromBody<string> providerUserId)
=> (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.RemoveExternalLogin(providerName, providerUserId, httpContext))).ToHttpResult(resultMapper))
.Accepts<ValueFromBody<string>>("application/json", "application/*+json")
.Produces<TokenResult>()
.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<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] ChangePasswordModel model) =>
{
return (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.ChangePassword(model, httpContext))).ToHttpResult(resultMapper);
})
.Accepts<ChangePasswordModel>("application/json", "application/*+json")
.Produces<TokenResult>()
.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<HttpIResult> resultMapper, HttpContext httpContext)
=> (await mediator.InvokeAsync<Result>(new AuthMessages.CheckEmailAddress(email, httpContext))).ToHttpResult(resultMapper))
.AllowAnonymous()
.ExcludeFromDescription();

group.MapGet("forgot-password/{email:minlength(1)}", async (string email, IMediator mediator, IMediatorResultMapper<HttpIResult> resultMapper, HttpContext httpContext)
=> (await mediator.InvokeAsync<Result>(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<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] ResetPasswordModel model) =>
{
return (await mediator.InvokeAsync<Result>(new AuthMessages.ResetPassword(model, httpContext))).ToHttpResult(resultMapper);
})
.AllowAnonymous()
.Accepts<ResetPasswordModel>("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<HttpIResult> resultMapper, HttpContext httpContext) =>
{
var contentTypeResult = ApiValidation.ValidateJsonContentType(httpContext.Request);
if (contentTypeResult is not null)
return contentTypeResult;

return (await mediator.InvokeAsync<Result>(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;
}
}
39 changes: 39 additions & 0 deletions src/Exceptionless.Web/Api/Endpoints/ContactEndpoints.cs
Original file line number Diff line number Diff line change
@@ -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<HttpIResult>(new SubmitContactRequest(request, httpContext)))
.AddEndpointFilter<AutoValidationEndpointFilter>()
.AllowAnonymous()
.Accepts<ContactRequest>("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<HttpIResult>(new SubmitContactRequest(request, httpContext)))
.AddEndpointFilter<AutoValidationEndpointFilter>()
.AllowAnonymous()
.DisableAntiforgery()
.Accepts<ContactRequest>("application/x-www-form-urlencoded", "multipart/form-data")
.Produces(StatusCodes.Status202Accepted)
.ProducesProblem(StatusCodes.Status422UnprocessableEntity)
.ProducesProblem(StatusCodes.Status429TooManyRequests)
.ProducesProblem(StatusCodes.Status503ServiceUnavailable)
.ExcludeFromDescription();

return endpoints;
}
}
Loading
Loading