291 lines
8.5 KiB
C#
291 lines
8.5 KiB
C#
using Microsoft.AspNetCore.Http.HttpResults;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using RpgRoller.Contracts;
|
|
using RpgRoller.Domain;
|
|
using RpgRoller.Services;
|
|
|
|
const string SessionCookieName = "rpgroller_session";
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
builder.Services.AddSingleton<IPasswordHasher<UserAccount>, PasswordHasher<UserAccount>>();
|
|
builder.Services.AddSingleton<IDiceRoller, RandomDiceRoller>();
|
|
builder.Services.AddSingleton<IGameService, GameService>();
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseDefaultFiles();
|
|
app.UseStaticFiles();
|
|
|
|
app.MapGet("/api/health", () => TypedResults.Ok(new HealthResponse("ok")));
|
|
app.MapGet("/api/rulesets", (IGameService game) => TypedResults.Ok(game.GetRulesets()));
|
|
|
|
app.MapPost("/api/auth/register", Results<Ok<UserSummary>, BadRequest<ApiError>> (RegisterRequest request, IGameService game) =>
|
|
{
|
|
var result = game.Register(request);
|
|
if (!result.Succeeded)
|
|
{
|
|
return ToBadRequest(result.Error!);
|
|
}
|
|
|
|
return TypedResults.Ok(result.Value!);
|
|
});
|
|
|
|
app.MapPost("/api/auth/login", Results<Ok<UserSummary>, BadRequest<ApiError>> (LoginRequest request, HttpContext context, IGameService game) =>
|
|
{
|
|
var result = game.Login(request);
|
|
if (!result.Succeeded)
|
|
{
|
|
return ToBadRequest(result.Error!);
|
|
}
|
|
|
|
context.Response.Cookies.Append(SessionCookieName, result.Value.SessionToken, new CookieOptions
|
|
{
|
|
HttpOnly = true,
|
|
SameSite = SameSiteMode.Strict,
|
|
IsEssential = true,
|
|
Secure = false
|
|
});
|
|
|
|
return TypedResults.Ok(result.Value.User);
|
|
});
|
|
|
|
app.MapPost("/api/auth/logout", (HttpContext context, IGameService game) =>
|
|
{
|
|
if (TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
game.Logout(sessionToken);
|
|
}
|
|
|
|
context.Response.Cookies.Delete(SessionCookieName);
|
|
return TypedResults.NoContent();
|
|
});
|
|
|
|
app.MapGet("/api/me", Results<Ok<MeResponse>, BadRequest<ApiError>, UnauthorizedHttpResult> (HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.GetMe(sessionToken);
|
|
if (!result.Succeeded)
|
|
{
|
|
return result.Error!.Code == "unauthorized"
|
|
? TypedResults.Unauthorized()
|
|
: TypedResults.BadRequest(new ApiError(result.Error.Message));
|
|
}
|
|
|
|
return TypedResults.Ok(result.Value!);
|
|
});
|
|
|
|
app.MapPost("/api/campaigns", (CreateCampaignRequest request, HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.CreateCampaign(sessionToken, request);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapGet("/api/campaigns", (HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.GetCampaigns(sessionToken);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapGet("/api/campaigns/{campaignId:guid}", (Guid campaignId, HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.GetCampaign(sessionToken, campaignId);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapGet("/api/campaigns/{campaignId:guid}/log", (Guid campaignId, HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.GetCampaignLog(sessionToken, campaignId);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapPost("/api/characters", (CreateCharacterRequest request, HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.CreateCharacter(sessionToken, request);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapPut("/api/characters/{characterId:guid}", (Guid characterId, UpdateCharacterRequest request, HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.UpdateCharacter(sessionToken, characterId, request);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapPost("/api/characters/{characterId:guid}/activate", (Guid characterId, HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.ActivateCharacter(sessionToken, characterId);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapGet("/api/characters/current-campaign", (HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.GetCurrentCampaignCharacters(sessionToken);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapPost("/api/characters/{characterId:guid}/skills", (Guid characterId, CreateSkillRequest request, HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.CreateSkill(sessionToken, characterId, request);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapPut("/api/skills/{skillId:guid}", (Guid skillId, UpdateSkillRequest request, HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.UpdateSkill(sessionToken, skillId, request);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapPost("/api/skills/{skillId:guid}/roll", (Guid skillId, RollSkillRequest request, HttpContext context, IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var result = game.RollSkill(sessionToken, skillId, request);
|
|
return ToApiResult(result);
|
|
});
|
|
|
|
app.MapGet("/api/events/state", async Task<IResult> (
|
|
Guid campaignId,
|
|
HttpContext context,
|
|
IGameService game) =>
|
|
{
|
|
if (!TryGetSessionToken(context, out var sessionToken))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var versionResult = game.GetCampaignVersion(sessionToken, campaignId);
|
|
if (!versionResult.Succeeded)
|
|
{
|
|
return versionResult.Error!.Code == "unauthorized"
|
|
? TypedResults.Unauthorized()
|
|
: TypedResults.BadRequest(new ApiError(versionResult.Error.Message));
|
|
}
|
|
|
|
context.Response.Headers.CacheControl = "no-cache";
|
|
context.Response.Headers.Connection = "keep-alive";
|
|
context.Response.ContentType = "text/event-stream";
|
|
|
|
var lastVersion = versionResult.Value;
|
|
await context.Response.WriteAsync($"event: state\ndata: {{\"campaignId\":\"{campaignId}\",\"version\":{lastVersion}}}\n\n");
|
|
await context.Response.Body.FlushAsync();
|
|
|
|
try
|
|
{
|
|
while (!context.RequestAborted.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(1), context.RequestAborted);
|
|
|
|
var currentVersionResult = game.GetCampaignVersion(sessionToken, campaignId);
|
|
if (!currentVersionResult.Succeeded)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (currentVersionResult.Value != lastVersion)
|
|
{
|
|
lastVersion = currentVersionResult.Value;
|
|
await context.Response.WriteAsync($"event: state\ndata: {{\"campaignId\":\"{campaignId}\",\"version\":{lastVersion}}}\n\n");
|
|
}
|
|
else
|
|
{
|
|
await context.Response.WriteAsync(": heartbeat\n\n");
|
|
}
|
|
|
|
await context.Response.Body.FlushAsync();
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
|
|
return TypedResults.Empty;
|
|
});
|
|
|
|
app.Run();
|
|
|
|
return;
|
|
|
|
static bool TryGetSessionToken(HttpContext context, out string sessionToken)
|
|
{
|
|
sessionToken = context.Request.Cookies[SessionCookieName] ?? string.Empty;
|
|
return !string.IsNullOrWhiteSpace(sessionToken);
|
|
}
|
|
|
|
static Results<Ok<T>, BadRequest<ApiError>, UnauthorizedHttpResult> ToApiResult<T>(ServiceResult<T> result)
|
|
{
|
|
if (result.Succeeded)
|
|
{
|
|
return TypedResults.Ok(result.Value!);
|
|
}
|
|
|
|
if (result.Error!.Code == "unauthorized")
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
return TypedResults.BadRequest(new ApiError(result.Error.Message));
|
|
}
|
|
|
|
static BadRequest<ApiError> ToBadRequest(ServiceError error)
|
|
{
|
|
return TypedResults.BadRequest(new ApiError(error.Message));
|
|
}
|
|
|
|
public partial class Program;
|