using System.Text.Json; using System.Text.Json.Serialization; using Socialize.Infrastructure.Security; using Socialize.Modules.Identity.Configuration; using Socialize.Modules.Identity.Data; using Socialize.Modules.Identity.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; namespace Socialize.Modules.Identity.Handlers; [PublicAPI] public class FacebookUserInfo { [JsonPropertyName("id")] public required string Id { get; init; } [JsonPropertyName("email")] public string? Email { get; init; } // Email might be null if not granted [JsonPropertyName("name")] public required string Name { get; init; } [JsonPropertyName("picture")] public required FacebookPictureData Picture { get; init; } } [PublicAPI] public class FacebookPictureData { [JsonPropertyName("data")] public required FacebookPicture Picture { get; init; } } [PublicAPI] public class FacebookPicture { [JsonPropertyName("url")] public required string Url { get; init; } } [PublicAPI] public record LoginWithFacebookRequest( string Token); [PublicAPI] public record LoginWithFacebookResponse( string AccessToken, string RefreshToken); [PublicAPI] public class LoginWithFacebookHandler( IHttpClientFactory httpClientFactory, UserManager userManager, IOptionsSnapshot jwtOptions, AccessTokenFactory accessTokenFactory) : Endpoint { public override void Configure() { AllowAnonymous(); Post("/api/users/login-with-facebook"); Options(o => o.WithTags("Users")); } public override async Task HandleAsync( LoginWithFacebookRequest request, CancellationToken ct) { // Verify the token with Facebook using HttpClient httpClient = httpClientFactory.CreateClient(); using HttpResponseMessage response = await httpClient.GetAsync( $"https://graph.facebook.com/me?access_token={request.Token}&fields=id,name,email,picture.width(200).height(200)", ct); if (!response.IsSuccessStatusCode) { await SendStringAsync( "The token is not valid", 400, cancellation: ct); return; } // Extract the user info (email, name, profile picture) string content = await response.Content.ReadAsStringAsync(ct); FacebookUserInfo? userInfo = JsonSerializer.Deserialize(content); if (userInfo is null || string.IsNullOrEmpty(userInfo.Id)) { await SendStringAsync( "Failed to retrieve user information from Facebook", 400, cancellation: ct); return; } // Check if user exists or create a new one User? user = await userManager.FindByEmailAsync(userInfo.Email!); if (user is null) { string generatedPassword = PasswordGenerator.Next(); User generatedUser = new() { UserName = userInfo.Email ?? $"fb_{userInfo.Id}", Email = userInfo.Email, EmailConfirmed = true, Firstname = userInfo.Name.Split(' ').FirstOrDefault() ?? "", Lastname = userInfo.Name.Split(' ').Skip(1).FirstOrDefault() ?? "", Alias = userInfo.Name, PortraitUrl = userInfo.Picture.Picture.Url, FacebookId = userInfo.Id // Storing Facebook ID }; IdentityResult result = await userManager.CreateAsync( generatedUser, generatedPassword); if (!result.Succeeded) { await SendStringAsync( result.Errors.First().Description, 400, cancellation: ct); return; } user = generatedUser; } // Generate refresh token string refreshToken = RefreshTokenGenerator.Next(); // Store refresh token in user's properties user.RefreshToken = refreshToken; user.RefreshTokenExpiryTime = DateTime.UtcNow.Add(jwtOptions.Value.RefreshTokenLifetime); await userManager.UpdateAsync(user); string accessToken = await accessTokenFactory.CreateAsync(user); await SendOkAsync( new LoginWithFacebookResponse(accessToken, refreshToken), ct); } }