using System.Text.Json; using System.Text.Json.Serialization; using Hutopy.Web.Common.Security; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; namespace Hutopy.Web.Features.Users.Handlers; 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; } } public class FacebookPictureData { [JsonPropertyName("data")] public required FacebookPicture Picture { get; init; } } 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, IdentityUserManager userManager, SignInManager signInManager, IOptionsSnapshot jwtOptions) : 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 var httpClient = httpClientFactory.CreateClient(); using var 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) var content = await response.Content.ReadAsStringAsync(ct); var 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 var user = await userManager.FindByEmailAsync(userInfo.Email!); if (user is null) { var generatedPassword = PasswordGenerator.GeneratePassword(10, 12); var generatedUser = new IdentityUser { UserName = userInfo.Email ?? $"fb_{userInfo.Id}", Email = userInfo.Email, 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 }; var result = await userManager.CreateAsync( generatedUser, generatedPassword); if (!result.Succeeded) { await SendStringAsync( result.Errors.First().Description, 400, cancellation: ct); return; } user = generatedUser; } await signInManager.SignInAsync(user, isPersistent: false); var accessToken = JwtTokenHelper.GenerateJwtToken( expiresIn: jwtOptions.Value.Lifetime, issuer: jwtOptions.Value.Issuer, audience: jwtOptions.Value.Audience, key: jwtOptions.Value.Key, userId: user.Id.ToString(), email: user.Email, alias: user.Alias, firstname: user.Firstname, lastname: user.Lastname, portraitUrl: user.PortraitUrl); await SendOkAsync( new LoginWithFacebookResponse(accessToken, string.Empty), cancellation: ct); } }