Tutorial

6.6 Sign in, and run trains as the caller

Program.cs:

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Shopfront.Auth;
using Shopfront.Data;
using Shopfront.Extensions;
using Shopfront.Trains.PlaceOrder;
using Trax.Api.Auth;
using Trax.Api.Auth.Jwt;
using Trax.Api.Exceptions;
using Trax.Api.Extensions;
using Trax.Core.Exceptions;
using Trax.Effect.Data.Extensions;
using Trax.Effect.Data.Postgres.Extensions;
using Trax.Effect.Data.Services.DomainContext;
using Trax.Effect.Extensions;
using Trax.Effect.JunctionProvider.Logging.Extensions;
using Trax.Effect.Provider.Parameter.Extensions;
using Trax.Mediator.Extensions;
using Trax.Mediator.Services.TrainExecution;
 
var builder = WebApplication.CreateBuilder(args);
 
var connectionString =
    builder.Configuration.GetConnectionString("Shopfront")
    ?? throw new InvalidOperationException("ConnectionStrings:Shopfront is not set.");
 
builder.Services.AddShopfront(connectionString);
 
if (builder.Environment.IsDevelopment())
    builder.Services.AddTraxJwtAuth(jwt =>
        jwt.UseSymmetricKey(DevJwt.Issuer, DevJwt.Audience, DevJwt.Key)
    );
else
    builder.Services.AddTraxJwtAuth(
        builder.Configuration["Auth:Authority"]
            ?? throw new InvalidOperationException("Auth:Authority is not set."),
        builder.Configuration["Auth:Audience"]
            ?? throw new InvalidOperationException("Auth:Audience is not set.")
    );
builder.Services.AddAuthorization();
 
builder.Services.AddTrax(trax =>
    trax.AddEffects(effects =>
            effects
                .UsePostgres(connectionString)
                .SaveTrainParameters()
                .AddJunctionLogger(serializeJunctionData: true)
                .AddDecisionRecording()
        )
        .AddMediator(typeof(Program).Assembly)
);
builder.Services.AddTraxApi();
 
var app = builder.Build();
 
await app.Services.EnsureSchemaCreatedAsync<ShopfrontDbContext>();
 
app.UseAuthentication();
app.UseAuthorization();
 
app.MapPost(
        "/orders",
        async (PlaceOrderInput input, ITrainExecutionService trains, CancellationToken ct) =>
        {
            try
            {
                var run = await trains.RunAsync(
                    typeof(IPlaceOrderTrain).FullName!,
                    JsonSerializer.Serialize(input),
                    ct
                );
                return Results.Ok(run.Output);
            }
            catch (TrainAuthorizationException)
            {
                return Results.StatusCode(StatusCodes.Status403Forbidden);
            }
            catch (TrainException ex)
            {
                return Results.UnprocessableEntity(ex.Message);
            }
        }
    )
    .RequireAuthorization(TraxAuthClaimTypes.TraxAuthPolicy);
 
app.MapGet(
        "/orders",
        async (IShopfrontDbContext db, CancellationToken ct) =>
            await db
                .Orders.OrderBy(o => o.PlacedAt)
                .Select(o => new
                {
                    o.Id,
                    o.CustomerId,
                    o.Total,
                    o.PlacedAt,
                })
                .ToListAsync(ct)
    )
    .RequireAuthorization(TraxAuthClaimTypes.TraxAuthPolicy);
 
if (app.Environment.IsDevelopment())
    app.MapGet(
            "/dev/token/{user}",
            (string user, bool staff = false) =>
                DevJwt.Mint(user, staff ? Roles.Staff : Roles.Customer)
        )
        .AllowAnonymous();
 
await app.RunAsync();

From the top:

  • AddTraxJwtAuth registers the JWT scheme as TraxJwt, and the services that turn a signed-in caller into TraxPrincipal and TraxCaller. In Development it checks tokens against the development key; anywhere else against an identity provider, read from configuration. Nothing else changes between the two, because the rest of the application sees only the principal.
  • AddTraxApi registers the service that checks [TraxAuthorize]. Leave it out and the host does not start; that is below.
  • RequireAuthorization(TraxAuthClaimTypes.TraxAuthPolicy) on each endpoint signs the caller in with Trax's schemes and refuses anyone who is not, with a 401. The Trax scheme is never the application's default, so an endpoint that says nothing signs nobody in.
  • POST /orders runs PlaceOrder through ITrainExecutionService, by its interface's name and with its input as JSON. That is the path that checks [TraxAuthorize]: a member of staff is signed in, so passes the endpoint, and is refused by the train with a 403.
  • GET /orders queries the orders. It has no Where: the query filter is the rule.
  • /dev/token/{user} mints a token, in Development only. Outside it, the route does not exist.

SDK Reference

AddTraxJwtAuth | TraxAuthorize | TraxPrincipal | ITrainExecutionService | Scope a data context to the caller | Effect data testing


Next: 6.7 Try it