Ajoute les amis et les invitations temps reel
This commit is contained in:
@@ -3,10 +3,12 @@ using System.Net.Mail;
|
||||
using ChessCubing.Server.Admin;
|
||||
using ChessCubing.Server.Auth;
|
||||
using ChessCubing.Server.Data;
|
||||
using ChessCubing.Server.Social;
|
||||
using ChessCubing.Server.Users;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -67,15 +69,21 @@ builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("AdminOnly", policy => policy.RequireRole("admin"));
|
||||
});
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddHttpClient<KeycloakAuthService>();
|
||||
builder.Services.AddSingleton<MySqlUserProfileStore>();
|
||||
builder.Services.AddSingleton<MySqlSocialStore>();
|
||||
builder.Services.AddSingleton<ConnectedUserTracker>();
|
||||
builder.Services.AddSingleton<PlayInviteCoordinator>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
await using (var scope = app.Services.CreateAsyncScope())
|
||||
{
|
||||
var profileStore = scope.ServiceProvider.GetRequiredService<MySqlUserProfileStore>();
|
||||
var socialStore = scope.ServiceProvider.GetRequiredService<MySqlSocialStore>();
|
||||
await profileStore.InitializeAsync(CancellationToken.None);
|
||||
await socialStore.InitializeAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
app.UseAuthentication();
|
||||
@@ -124,6 +132,169 @@ app.MapPut("/api/users/me", async Task<IResult> (
|
||||
}
|
||||
}).RequireAuthorization();
|
||||
|
||||
var socialGroup = app.MapGroup("/api/social")
|
||||
.RequireAuthorization();
|
||||
|
||||
socialGroup.MapGet("/overview", async Task<IResult> (
|
||||
ClaimsPrincipal user,
|
||||
MySqlSocialStore socialStore,
|
||||
ConnectedUserTracker tracker,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var siteUser = AuthenticatedSiteUserFactory.FromClaimsPrincipal(user);
|
||||
if (siteUser is null)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse("La session utilisateur est incomplete."));
|
||||
}
|
||||
|
||||
var overview = await socialStore.GetOverviewAsync(siteUser.Subject, tracker.IsOnline, cancellationToken);
|
||||
return TypedResults.Ok(overview);
|
||||
});
|
||||
|
||||
socialGroup.MapGet("/search", async Task<IResult> (
|
||||
string? query,
|
||||
ClaimsPrincipal user,
|
||||
MySqlSocialStore socialStore,
|
||||
ConnectedUserTracker tracker,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var siteUser = AuthenticatedSiteUserFactory.FromClaimsPrincipal(user);
|
||||
if (siteUser is null)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse("La session utilisateur est incomplete."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var results = await socialStore.SearchUsersAsync(siteUser.Subject, query, tracker.IsOnline, cancellationToken);
|
||||
return TypedResults.Ok(results);
|
||||
}
|
||||
catch (SocialValidationException exception)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse(exception.Message));
|
||||
}
|
||||
});
|
||||
|
||||
socialGroup.MapPost("/invitations", async Task<IResult> (
|
||||
SendFriendInvitationRequest request,
|
||||
ClaimsPrincipal user,
|
||||
MySqlSocialStore socialStore,
|
||||
IHubContext<SocialHub> hubContext,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var siteUser = AuthenticatedSiteUserFactory.FromClaimsPrincipal(user);
|
||||
if (siteUser is null)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse("La session utilisateur est incomplete."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var targetSubject = request.TargetSubject?.Trim() ?? string.Empty;
|
||||
await socialStore.SendInvitationAsync(siteUser.Subject, targetSubject, cancellationToken);
|
||||
await NotifySocialChangedAsync(hubContext, siteUser.Subject, targetSubject);
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
catch (SocialValidationException exception)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse(exception.Message));
|
||||
}
|
||||
});
|
||||
|
||||
socialGroup.MapPost("/invitations/{invitationId:long}/accept", async Task<IResult> (
|
||||
long invitationId,
|
||||
ClaimsPrincipal user,
|
||||
MySqlSocialStore socialStore,
|
||||
IHubContext<SocialHub> hubContext,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var siteUser = AuthenticatedSiteUserFactory.FromClaimsPrincipal(user);
|
||||
if (siteUser is null)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse("La session utilisateur est incomplete."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var senderSubject = await socialStore.AcceptInvitationAsync(invitationId, siteUser.Subject, cancellationToken);
|
||||
await NotifySocialChangedAsync(hubContext, siteUser.Subject, senderSubject);
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
catch (SocialValidationException exception)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse(exception.Message));
|
||||
}
|
||||
});
|
||||
|
||||
socialGroup.MapPost("/invitations/{invitationId:long}/decline", async Task<IResult> (
|
||||
long invitationId,
|
||||
ClaimsPrincipal user,
|
||||
MySqlSocialStore socialStore,
|
||||
IHubContext<SocialHub> hubContext,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var siteUser = AuthenticatedSiteUserFactory.FromClaimsPrincipal(user);
|
||||
if (siteUser is null)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse("La session utilisateur est incomplete."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var senderSubject = await socialStore.DeclineInvitationAsync(invitationId, siteUser.Subject, cancellationToken);
|
||||
await NotifySocialChangedAsync(hubContext, siteUser.Subject, senderSubject);
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
catch (SocialValidationException exception)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse(exception.Message));
|
||||
}
|
||||
});
|
||||
|
||||
socialGroup.MapDelete("/invitations/{invitationId:long}", async Task<IResult> (
|
||||
long invitationId,
|
||||
ClaimsPrincipal user,
|
||||
MySqlSocialStore socialStore,
|
||||
IHubContext<SocialHub> hubContext,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var siteUser = AuthenticatedSiteUserFactory.FromClaimsPrincipal(user);
|
||||
if (siteUser is null)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse("La session utilisateur est incomplete."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var recipientSubject = await socialStore.CancelInvitationAsync(invitationId, siteUser.Subject, cancellationToken);
|
||||
await NotifySocialChangedAsync(hubContext, siteUser.Subject, recipientSubject);
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
catch (SocialValidationException exception)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse(exception.Message));
|
||||
}
|
||||
});
|
||||
|
||||
socialGroup.MapDelete("/friends/{friendSubject}", async Task<IResult> (
|
||||
string friendSubject,
|
||||
ClaimsPrincipal user,
|
||||
MySqlSocialStore socialStore,
|
||||
IHubContext<SocialHub> hubContext,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var siteUser = AuthenticatedSiteUserFactory.FromClaimsPrincipal(user);
|
||||
if (siteUser is null)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiErrorResponse("La session utilisateur est incomplete."));
|
||||
}
|
||||
|
||||
var normalizedFriendSubject = friendSubject.Trim();
|
||||
await socialStore.RemoveFriendAsync(siteUser.Subject, normalizedFriendSubject, cancellationToken);
|
||||
await NotifySocialChangedAsync(hubContext, siteUser.Subject, normalizedFriendSubject);
|
||||
return TypedResults.NoContent();
|
||||
});
|
||||
|
||||
var adminGroup = app.MapGroup("/api/admin")
|
||||
.RequireAuthorization("AdminOnly");
|
||||
|
||||
@@ -230,11 +401,13 @@ adminGroup.MapDelete("/users/{subject}", async Task<IResult> (
|
||||
string subject,
|
||||
KeycloakAuthService keycloak,
|
||||
MySqlUserProfileStore profileStore,
|
||||
MySqlSocialStore socialStore,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await keycloak.DeleteAdminUserAsync(subject, cancellationToken);
|
||||
await socialStore.DeleteUserAsync(subject, cancellationToken);
|
||||
await profileStore.DeleteAsync(subject, cancellationToken);
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
@@ -307,6 +480,7 @@ app.MapPost("/api/auth/login", async Task<IResult> (
|
||||
LoginRequest request,
|
||||
HttpContext httpContext,
|
||||
KeycloakAuthService keycloak,
|
||||
MySqlUserProfileStore profileStore,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
|
||||
@@ -318,6 +492,7 @@ app.MapPost("/api/auth/login", async Task<IResult> (
|
||||
{
|
||||
var userInfo = await keycloak.LoginAsync(request.Username.Trim(), request.Password, cancellationToken);
|
||||
await SignInAsync(httpContext, userInfo);
|
||||
await EnsureSiteUserAsync(profileStore, userInfo, cancellationToken);
|
||||
return TypedResults.Ok(AuthSessionResponse.FromUser(httpContext.User));
|
||||
}
|
||||
catch (KeycloakAuthException exception)
|
||||
@@ -330,6 +505,7 @@ app.MapPost("/api/auth/register", async Task<IResult> (
|
||||
RegisterRequest request,
|
||||
HttpContext httpContext,
|
||||
KeycloakAuthService keycloak,
|
||||
MySqlUserProfileStore profileStore,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Username) ||
|
||||
@@ -355,6 +531,7 @@ app.MapPost("/api/auth/register", async Task<IResult> (
|
||||
}, cancellationToken);
|
||||
|
||||
await SignInAsync(httpContext, userInfo);
|
||||
await EnsureSiteUserAsync(profileStore, userInfo, cancellationToken);
|
||||
return TypedResults.Ok(AuthSessionResponse.FromUser(httpContext.User));
|
||||
}
|
||||
catch (KeycloakAuthException exception)
|
||||
@@ -375,6 +552,9 @@ app.MapGet("/api/auth/logout/browser", async Task<IResult> (HttpContext httpCont
|
||||
return TypedResults.Redirect("/index.html");
|
||||
});
|
||||
|
||||
app.MapHub<SocialHub>("/hubs/social")
|
||||
.RequireAuthorization();
|
||||
|
||||
app.Run();
|
||||
|
||||
static AdminUserSummaryResponse MapAdminSummary(AdminIdentityUser identity, UserProfileResponse? profile)
|
||||
@@ -577,6 +757,32 @@ static async Task SignInAsync(HttpContext httpContext, KeycloakUserInfo userInfo
|
||||
httpContext.User = principal;
|
||||
}
|
||||
|
||||
static async Task EnsureSiteUserAsync(
|
||||
MySqlUserProfileStore profileStore,
|
||||
KeycloakUserInfo userInfo,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var siteUser = AuthenticatedSiteUserFactory.FromKeycloakUserInfo(userInfo);
|
||||
if (siteUser is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await profileStore.GetOrCreateAsync(siteUser, cancellationToken);
|
||||
}
|
||||
|
||||
static Task NotifySocialChangedAsync(IHubContext<SocialHub> hubContext, params string[] subjects)
|
||||
{
|
||||
var distinctSubjects = subjects
|
||||
.Where(subject => !string.IsNullOrWhiteSpace(subject))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
return distinctSubjects.Length == 0
|
||||
? Task.CompletedTask
|
||||
: hubContext.Clients.Users(distinctSubjects).SendAsync("SocialChanged");
|
||||
}
|
||||
|
||||
sealed record NormalizedAdminUserUpdate(
|
||||
string? Email,
|
||||
string? FirstName,
|
||||
|
||||
Reference in New Issue
Block a user