diff --git a/backend/src/Web/Features/Users/Data/Migrations/20250217034117_Add_FacebookId.Designer.cs b/backend/src/Web/Features/Users/Data/Migrations/20250217034117_Add_FacebookId.Designer.cs new file mode 100644 index 0000000..574c2dc --- /dev/null +++ b/backend/src/Web/Features/Users/Data/Migrations/20250217034117_Add_FacebookId.Designer.cs @@ -0,0 +1,308 @@ +// +using System; +using Hutopy.Web.Features.Users.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Hutopy.Web.Features.Users.Data.Migrations +{ + [DbContext(typeof(IdentityDbContext))] + [Migration("20250217034117_Add_FacebookId")] + partial class Add_FacebookId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Identity") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Web.Features.Users.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", "Identity"); + }); + + modelBuilder.Entity("Hutopy.Web.Features.Users.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("Address") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Alias") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FacebookId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Firstname") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("GoogleId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Lastname") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PortraitUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", "Identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Hutopy.Web.Features.Users.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Hutopy.Web.Features.Users.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Hutopy.Web.Features.Users.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Web/Features/Users/Data/Migrations/20250217034117_Add_FacebookId.cs b/backend/src/Web/Features/Users/Data/Migrations/20250217034117_Add_FacebookId.cs new file mode 100644 index 0000000..e3248e3 --- /dev/null +++ b/backend/src/Web/Features/Users/Data/Migrations/20250217034117_Add_FacebookId.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Web.Features.Users.Data.Migrations +{ + /// + public partial class Add_FacebookId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FacebookId", + schema: "Identity", + table: "AspNetUsers", + type: "character varying(255)", + maxLength: 255, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FacebookId", + schema: "Identity", + table: "AspNetUsers"); + } + } +} diff --git a/backend/src/Web/Features/Users/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/src/Web/Features/Users/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 0e94432..c139c9e 100644 --- a/backend/src/Web/Features/Users/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/src/Web/Features/Users/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -81,6 +81,10 @@ namespace Hutopy.Web.Features.Users.Data.Migrations b.Property("EmailConfirmed") .HasColumnType("boolean"); + b.Property("FacebookId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("Firstname") .HasMaxLength(255) .HasColumnType("character varying(255)"); diff --git a/backend/src/Web/Features/Users/Handlers/LoginWithFacebook.cs b/backend/src/Web/Features/Users/Handlers/LoginWithFacebook.cs new file mode 100644 index 0000000..ba3f261 --- /dev/null +++ b/backend/src/Web/Features/Users/Handlers/LoginWithFacebook.cs @@ -0,0 +1,132 @@ +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); + } +} diff --git a/backend/src/Web/Features/Users/IdentityUser.cs b/backend/src/Web/Features/Users/IdentityUser.cs index e648674..0819994 100644 --- a/backend/src/Web/Features/Users/IdentityUser.cs +++ b/backend/src/Web/Features/Users/IdentityUser.cs @@ -4,12 +4,14 @@ using Microsoft.AspNetCore.Identity; namespace Hutopy.Web.Features.Users; public class IdentityUser : IdentityUser -{ +{ [MaxLength(255)] public string? Alias { get; set; } [MaxLength(255)] public string? Firstname { get; set; } [MaxLength(255)] public string? Lastname { get; set; } - public DateTime? BirthDate { get; set; } - [MaxLength(255)] public string? Address { get; set; } + public DateTime? BirthDate { get; set; } + [MaxLength(255)] public string? Address { get; set; } [MaxLength(255)] public string? PortraitUrl { get; set; } [MaxLength(255)] public string? GoogleId { get; set; } + [MaxLength(255)] public string? FacebookId { get; set; } } + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9dedfca..81a53de 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@stripe/stripe-js": "^3.0.10", "@tinymce/tinymce-vue": "^6.0.1", "@vueuse/core": "^10.11.0", + "@vueuse/head": "^2.0.0", "@xtiannyeto/vue-auth-social": "^0.1.9", "axios": "^1.6.7", "i18n": "^0.15.1", @@ -1014,16 +1015,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fortawesome/fontawesome-free": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", - "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", - "dev": true, - "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", - "engines": { - "node": ">=6" - } - }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -1611,16 +1602,6 @@ "vue": "^3.0.0" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/configstore": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/configstore/-/configstore-2.1.1.tgz", @@ -1730,6 +1711,76 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@unhead/dom": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.19.tgz", + "integrity": "sha512-udkgITdIblEWH3hsoFQMKW+6QXNO2qFZlZ2FI37bVAplQSnK/PytTPt/5oA1GWkoVwT0DsQNGHbU6kOg/3SlNg==", + "license": "MIT", + "dependencies": { + "@unhead/schema": "1.11.19", + "@unhead/shared": "1.11.19" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/schema": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.19.tgz", + "integrity": "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg==", + "license": "MIT", + "dependencies": { + "hookable": "^5.5.3", + "zhead": "^2.2.4" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/shared": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.19.tgz", + "integrity": "sha512-UYE9EIeQLJOhx8vC71bWGkAGY4Zzq/H8qYlihowUg4NiFOfL+KKMnj96datb74PRxSDvHac9V3OLktNcsX2NuA==", + "license": "MIT", + "dependencies": { + "@unhead/schema": "1.11.19", + "packrup": "^0.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/ssr": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/@unhead/ssr/-/ssr-1.11.19.tgz", + "integrity": "sha512-OH+rj6xBTdYyLsSntk4lEQyR+z57aEUZIiR2UpPl1zWGtBZPIr5zs3GY5+EyJ8t8e0zLemPR/Pu7VembTJ8o1w==", + "license": "MIT", + "dependencies": { + "@unhead/schema": "1.11.19", + "@unhead/shared": "1.11.19" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/vue": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.11.19.tgz", + "integrity": "sha512-/XATTP8wVLs3+2Pkj2crvr/Z55nybVQyOwISh+sAlr/48/9n3jGNiCZHKpHgL4MpOnGT4krwzWzbfhBO/G2BSQ==", + "license": "MIT", + "dependencies": { + "@unhead/schema": "1.11.19", + "@unhead/shared": "1.11.19", + "hookable": "^5.5.3", + "unhead": "1.11.19" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "vue": ">=2.7 || >=3" + } + }, "node_modules/@vee-validate/rules": { "version": "4.13.2", "resolved": "https://registry.npmjs.org/@vee-validate/rules/-/rules-4.13.2.tgz", @@ -1886,6 +1937,21 @@ } } }, + "node_modules/@vueuse/head": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/head/-/head-2.0.0.tgz", + "integrity": "sha512-ykdOxTGs95xjD4WXE4na/umxZea2Itl0GWBILas+O4oqS7eXIods38INvk3XkJKjqMdWPcpCyLX/DioLQxU1KA==", + "license": "MIT", + "dependencies": { + "@unhead/dom": "^1.7.0", + "@unhead/schema": "^1.7.0", + "@unhead/ssr": "^1.7.0", + "@unhead/vue": "^1.7.0" + }, + "peerDependencies": { + "vue": ">=2.7 || >=3" + } + }, "node_modules/@vueuse/metadata": { "version": "10.11.1", "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", @@ -2935,50 +3001,6 @@ "node": "*" } }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2990,42 +3012,6 @@ "node": ">=4" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3306,65 +3292,6 @@ "node": ">=6.0.0" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/dot-prop": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", @@ -4464,6 +4391,12 @@ "node": ">= 0.4" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -5302,13 +5235,6 @@ "is-buffer": "~1.1.6" } }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5864,6 +5790,15 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, + "node_modules/packrup": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/packrup/-/packrup-0.1.2.tgz", + "integrity": "sha512-ZcKU7zrr5GlonoS9cxxrb5HVswGnyj6jQvwFBa6p5VFw7G71VAHcUKL5wyZSU/ECtPM/9gacWxy2KFQKt1gMNA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7143,42 +7078,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/tailwindcss": { "version": "3.4.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", @@ -7393,6 +7292,21 @@ "dev": true, "license": "MIT" }, + "node_modules/unhead": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-1.11.19.tgz", + "integrity": "sha512-O5AYb3+xUOzBlwDmPfC/DgGp9rDMoGkB4gFkhoaz8IonQqP8W8qqetxYf5ZyEdntvXnFsMWS8lZF//5176xo6Q==", + "license": "MIT", + "dependencies": { + "@unhead/dom": "1.11.19", + "@unhead/schema": "1.11.19", + "@unhead/shared": "1.11.19", + "hookable": "^5.5.3" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -7590,19 +7504,6 @@ } } }, - "node_modules/vite-svg-loader": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-5.1.0.tgz", - "integrity": "sha512-M/wqwtOEjgb956/+m5ZrYT/Iq6Hax0OakWbokj8+9PXOnB7b/4AxESHieEtnNEy7ZpjsjYW1/5nK8fATQMmRxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "svgo": "^3.0.2" - }, - "peerDependencies": { - "vue": ">=3.2.13" - } - }, "node_modules/vue": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.38.tgz", @@ -8085,6 +7986,15 @@ "engines": { "node": ">=10" } + }, + "node_modules/zhead": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", + "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } } } } diff --git a/frontend/package.json b/frontend/package.json index bc85f07..e1ec3a0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "@stripe/stripe-js": "^3.0.10", "@tinymce/tinymce-vue": "^6.0.1", "@vueuse/core": "^10.11.0", + "@vueuse/head": "^2.0.0", "@xtiannyeto/vue-auth-social": "^0.1.9", "axios": "^1.6.7", "i18n": "^0.15.1", diff --git a/frontend/src/composables/useFacebookLogin.js b/frontend/src/composables/useFacebookLogin.js new file mode 100644 index 0000000..ae5f179 --- /dev/null +++ b/frontend/src/composables/useFacebookLogin.js @@ -0,0 +1,76 @@ +import {onMounted, ref} from "vue"; +import {useHead} from "@vueuse/head"; +import {useAuthStore} from "@/stores/authStore.js"; + +export function useFacebookLogin() { + const isSdkLoaded = ref(false); + + /* TODO: FIND THE ACTUAL HUTOPY'S APP_ID */ + const FACEBOOK_APP_ID = "1076433907621883"; + + useHead({ + script: [ + { + src: "https://connect.facebook.net/en_US/sdk.js", + async: true, + defer: true, + onload: "initializeFacebookSDK()", + }, + ], + }); + + (function (d, s, id) { + var js, fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) { + return; + } + js = d.createElement(s); + js.id = id; + js.src = "https://connect.facebook.net/en_US/sdk.js"; + fjs.parentNode.insertBefore(js, fjs); + }(document, 'script', 'facebook-jssdk')); + + const initializeFacebookSDK = () => { + window.fbAsyncInit = function () { + FB.init({ + appId: FACEBOOK_APP_ID, + xfbml: true, + version: 'v22.0' + }); + FB.AppEvents.logPageView(); + + isSdkLoaded.value = true; + }; + + }; + + const loginWithFacebook = () => { + if (!isSdkLoaded.value) { + console.error("Facebook SDK non encore chargé !"); + return; + } + + window.FB.login( + (response) => { + if (response.authResponse) { + console.log("Utilisateur connecté :", response); + const authStore = useAuthStore(); + authStore.loginWithFacebook(response.authResponse); + } else { + console.log("Connexion annulée ou échouée."); + } + }, + { + scope: "public_profile,email" + } + ); + }; + + onMounted(() => { + initializeFacebookSDK(); + }); + + return { + loginWithFacebook, + }; +} diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index c74f07d..525949d 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -1,39 +1,13 @@ - - - - - - - - - - - Connexion - - - - - mdi-google - Google - - - - - - - - - - - + + + + + + + + + + + + Connexion + + + + + + mdi-google + Google + + + + + + Facebook + + + + + + + + + + + +