LoginWithGoogle feature now working.
This commit is contained in:
@@ -11,4 +11,5 @@ public class ApplicationUser : IdentityUser<Guid>
|
|||||||
public DateTime? BirthDate { get; set; }
|
public DateTime? BirthDate { get; set; }
|
||||||
[MaxLength(255)] public string? Address { get; set; }
|
[MaxLength(255)] public string? Address { get; set; }
|
||||||
[MaxLength(255)] public string? PortraitUrl { get; set; }
|
[MaxLength(255)] public string? PortraitUrl { get; set; }
|
||||||
|
[MaxLength(255)] public string? GoogleId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
435
src/Infrastructure/Migrations/20240924044131_AddGoogleId.Designer.cs
generated
Normal file
435
src/Infrastructure/Migrations/20240924044131_AddGoogleId.Designer.cs
generated
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Hutopy.Infrastructure.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20240924044131_AddGoogleId")]
|
||||||
|
partial class AddGoogleId
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.4")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Domain.Entities.FutureCreator", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CreatedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("EmailAddress")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastModifiedAt")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<Guid?>("LastModifiedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ReasonToJoin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("SocialNetworkAccount")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("FutureCreators");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<Guid>("ApplicationUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CreatedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Currency")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastModifiedAt")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<Guid?>("LastModifiedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<bool>("Paid")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("StripeBillingDetailEmail")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeBillingDetailName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeChargeId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeEventId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("StripePaymentIntent")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("StripePaymentMethod")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeReceiptUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("TipMessage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ApplicationUserId");
|
||||||
|
|
||||||
|
b.ToTable("UserTransactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("Alias")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("BirthDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Firstname")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("Lastname")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("PortraitUrl")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ApplicationUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/Infrastructure/Migrations/20240924044131_AddGoogleId.cs
Normal file
29
src/Infrastructure/Migrations/20240924044131_AddGoogleId.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddGoogleId : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "GoogleId",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "nvarchar(255)",
|
||||||
|
maxLength: 255,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GoogleId",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -189,7 +189,6 @@ namespace Hutopy.Infrastructure.Migrations
|
|||||||
.HasColumnType("nvarchar(255)");
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
b.Property<DateTime?>("BirthDate")
|
b.Property<DateTime?>("BirthDate")
|
||||||
.HasMaxLength(255)
|
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
b.Property<string>("ConcurrencyStamp")
|
||||||
@@ -207,6 +206,10 @@ namespace Hutopy.Infrastructure.Migrations
|
|||||||
.HasMaxLength(255)
|
.HasMaxLength(255)
|
||||||
.HasColumnType("nvarchar(255)");
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
b.Property<string>("Lastname")
|
b.Property<string>("Lastname")
|
||||||
.HasMaxLength(255)
|
.HasMaxLength(255)
|
||||||
.HasColumnType("nvarchar(255)");
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|||||||
70
src/Infrastructure/Utils/PasswordGenerator.cs
Normal file
70
src/Infrastructure/Utils/PasswordGenerator.cs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure.Utils;
|
||||||
|
|
||||||
|
// If we need to add special characters we can alternate between 2 pools.
|
||||||
|
public class PasswordGenerator
|
||||||
|
{
|
||||||
|
private const string LowerLetters = "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
private const string UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
private const string Numbers = "0123456789";
|
||||||
|
private const string SpecialCharacters = "!@#$%^&*()_+-=[];',./`~{}|:\"<>?";
|
||||||
|
|
||||||
|
private static readonly Random Random = new();
|
||||||
|
|
||||||
|
public static string GeneratePassword(
|
||||||
|
int minLength,
|
||||||
|
int maxLength,
|
||||||
|
bool requireNumber = true,
|
||||||
|
bool requireCapital = true,
|
||||||
|
bool requireSpecialCharacter = true)
|
||||||
|
{
|
||||||
|
// Create pools based on the requirements
|
||||||
|
var characterPool = new StringBuilder(LowerLetters);
|
||||||
|
|
||||||
|
if (requireCapital)
|
||||||
|
characterPool.Append(UpperLetters);
|
||||||
|
|
||||||
|
if (requireNumber)
|
||||||
|
characterPool.Append(Numbers);
|
||||||
|
|
||||||
|
if (requireSpecialCharacter)
|
||||||
|
characterPool.Append(SpecialCharacters);
|
||||||
|
|
||||||
|
// Ensure that the length is within the specified bounds
|
||||||
|
int length = Random.Next(minLength, maxLength + 1);
|
||||||
|
var password = new char[length];
|
||||||
|
|
||||||
|
// Ensure at least one character from each required category is included
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
if (requireCapital)
|
||||||
|
password[index++] = UpperLetters[Random.Next(UpperLetters.Length)];
|
||||||
|
|
||||||
|
if (requireNumber)
|
||||||
|
password[index++] = Numbers[Random.Next(Numbers.Length)];
|
||||||
|
|
||||||
|
if (requireSpecialCharacter)
|
||||||
|
password[index++] = SpecialCharacters[Random.Next(SpecialCharacters.Length)];
|
||||||
|
|
||||||
|
// Fill the rest of the password
|
||||||
|
for (int i = index; i < length; i++)
|
||||||
|
{
|
||||||
|
password[i] = characterPool[Random.Next(characterPool.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle the password to randomize the placement of the required characters
|
||||||
|
Shuffle(password);
|
||||||
|
return new string(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Shuffle(
|
||||||
|
char[] array)
|
||||||
|
{
|
||||||
|
for (int i = array.Length - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
int j = Random.Next(i + 1);
|
||||||
|
(array[i], array[j]) = (array[j], array[i]); // Swap elements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Hutopy.Infrastructure.Utils;
|
|
||||||
|
|
||||||
// If we need to add special characters we can alternate between 2 pools.
|
|
||||||
public class RandomGenerator
|
|
||||||
{
|
|
||||||
// For the moment, numbers and special characters don't work because
|
|
||||||
// the random generator is designed to handle a single integer.
|
|
||||||
// We can modify this in the future.
|
|
||||||
private const string LetterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
+ "0123456789"
|
|
||||||
+ "!@#$%^&*()_+"
|
|
||||||
+ "-=[];',./`~{}|:\"<>?";
|
|
||||||
private const int LetterIdxBits = 6;
|
|
||||||
private const int LetterIdxMask = 1 << LetterIdxBits;
|
|
||||||
private const int LetterIdxMax = 64 / LetterIdxBits;
|
|
||||||
|
|
||||||
private static readonly Random Src = new();
|
|
||||||
|
|
||||||
public static byte[] RandBytesMaskSrc(int n)
|
|
||||||
{
|
|
||||||
var b = new byte[n];
|
|
||||||
|
|
||||||
for (var i = n - 1; i >= 0;)
|
|
||||||
{
|
|
||||||
long cache = Src.NextInt64();
|
|
||||||
int remain = LetterIdxMax;
|
|
||||||
|
|
||||||
while (remain != 0)
|
|
||||||
{
|
|
||||||
if (i < 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (cache == 0)
|
|
||||||
cache = Src.NextInt64();
|
|
||||||
|
|
||||||
var idx = (int)(cache & LetterIdxMask);
|
|
||||||
if (idx < LetterBytes.Length)
|
|
||||||
{
|
|
||||||
b[i] = (byte)LetterBytes[idx];
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
|
|
||||||
cache >>= LetterIdxBits;
|
|
||||||
remain--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string RandomString(int length)
|
|
||||||
{
|
|
||||||
var bytes = RandBytesMaskSrc(length);
|
|
||||||
return Encoding.UTF8.GetString(bytes); // Equivalent for *(string*)(&bytes[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -49,7 +49,7 @@ public class FacebookController(IIdentityService identityService) : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
await identityService.CreateUserAsync(email, givenName, givenName, familyName,
|
await identityService.CreateUserAsync(email, givenName, givenName, familyName,
|
||||||
RandomGenerator.RandomString(24));
|
PasswordGenerator.GeneratePassword(8, 10));
|
||||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
|
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
|
||||||
new ClaimsPrincipal(claimsIdentity));
|
new ClaimsPrincipal(claimsIdentity));
|
||||||
return Redirect("/");
|
return Redirect("/");
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using Hutopy.Application.Common.Interfaces;
|
|
||||||
using Hutopy.Infrastructure.Identity;
|
|
||||||
using Hutopy.Infrastructure.Utils;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace Hutopy.Web.Controllers;
|
|
||||||
|
|
||||||
public class GoogleController(
|
|
||||||
IIdentityService identityService,
|
|
||||||
IHttpClientFactory httpClientFactory,
|
|
||||||
IOptionsSnapshot<JwtOptions> jwtOptions)
|
|
||||||
: Controller
|
|
||||||
{
|
|
||||||
[Microsoft.AspNetCore.Mvc.HttpPost("/api/google/sign-in")]
|
|
||||||
public async Task<IActionResult> SignIn([Microsoft.AspNetCore.Mvc.FromBody] GoogleSignInRequest request)
|
|
||||||
{
|
|
||||||
using var httpClient = httpClientFactory.CreateClient();
|
|
||||||
|
|
||||||
// Verify the token with Google
|
|
||||||
var response = await httpClient.GetAsync($"https://www.googleapis.com/oauth2/v1/userinfo?access_token={request.AccessToken}");
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return BadRequest("Invalid Google token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var userInfo = JObject.Parse(await response.Content.ReadAsStringAsync());
|
|
||||||
var email = userInfo["email"]?.ToString() ?? "";
|
|
||||||
var name = userInfo["name"]?.ToString() ?? "";
|
|
||||||
var givenName = userInfo["given_name"]?.ToString() ?? "";
|
|
||||||
var familyName = userInfo["family_name"]?.ToString() ?? "";
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(email))
|
|
||||||
{
|
|
||||||
return BadRequest("Google token did not contain an email.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user exists or create a new one
|
|
||||||
var user = await identityService.FindUserByEmailAsync(email);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
await identityService.CreateUserAsync(email, email, givenName, familyName, RandomGenerator.RandomString(24));
|
|
||||||
user = await identityService.FindUserByEmailAsync(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user?.Id is null)
|
|
||||||
{
|
|
||||||
return BadRequest("Unable to find or create the user.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign in the user
|
|
||||||
var claimsIdentity = new ClaimsIdentity(
|
|
||||||
new List<Claim>
|
|
||||||
{
|
|
||||||
new(ClaimTypes.Name, name),
|
|
||||||
new(ClaimTypes.Email, email),
|
|
||||||
new(ClaimTypes.GivenName, givenName),
|
|
||||||
new(ClaimTypes.Surname, familyName)
|
|
||||||
},
|
|
||||||
CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
|
|
||||||
await HttpContext.SignInAsync(
|
|
||||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
|
||||||
new ClaimsPrincipal(claimsIdentity));
|
|
||||||
|
|
||||||
var token = JwtTokenHelper.GenerateJwtToken(
|
|
||||||
jwtOptions.Value.Lifetime,
|
|
||||||
jwtOptions.Value.Issuer,
|
|
||||||
jwtOptions.Value.Audience,
|
|
||||||
jwtOptions.Value.Key,
|
|
||||||
user.Id.ToString(),
|
|
||||||
user.Email,
|
|
||||||
user.Alias,
|
|
||||||
user.Firstname,
|
|
||||||
user.Lastname,
|
|
||||||
user.PortraitUrl);
|
|
||||||
|
|
||||||
return Ok(new { accessToken = token, email });
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GoogleSignInRequest
|
|
||||||
{
|
|
||||||
public required string AccessToken { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
140
src/Web/Features/Users/Handlers/LoginWithGoogle.cs
Normal file
140
src/Web/Features/Users/Handlers/LoginWithGoogle.cs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Hutopy.Infrastructure.Identity;
|
||||||
|
using Hutopy.Infrastructure.Utils;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Hutopy.Web.Features.Users.Handlers;
|
||||||
|
|
||||||
|
class GoogleToken
|
||||||
|
{
|
||||||
|
[JsonPropertyName("access_token")] public required string AccessToken { get; init; }
|
||||||
|
[JsonPropertyName("token_type")] public required string TokenType { get; init; }
|
||||||
|
[JsonPropertyName("expires_in")] public required int ExpiresIn { get; init; }
|
||||||
|
[JsonPropertyName("scope")] public required string Scope { get; init; }
|
||||||
|
[JsonPropertyName("authuser")] public required string AuthUser { get; init; }
|
||||||
|
[JsonPropertyName("prompt")] public required string Prompt { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GoogleUserInfo
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public required string Id { get; init; }
|
||||||
|
[JsonPropertyName("email")] public required string Email { get; init; }
|
||||||
|
[JsonPropertyName("verified_email")] public required bool VerifiedEmail { get; init; }
|
||||||
|
[JsonPropertyName("name")] public required string Name { get; init; }
|
||||||
|
[JsonPropertyName("given_name")] public required string GivenName { get; init; }
|
||||||
|
[JsonPropertyName("family_name")] public required string FamilyName { get; init; }
|
||||||
|
[JsonPropertyName("picture")] public required string Picture { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public record LoginWithGoogleRequest(
|
||||||
|
string Token)
|
||||||
|
: IRequest<LoginWithGoogleResponse>;
|
||||||
|
|
||||||
|
public record LoginWithGoogleResponse(
|
||||||
|
string AccessToken,
|
||||||
|
string RefreshToken);
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public class LoginWithGoogleHandler(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
ApplicationUserManager userManager,
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
IOptionsSnapshot<JwtOptions> jwtOptions)
|
||||||
|
: Endpoint<LoginWithGoogleRequest, LoginWithGoogleResponse>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
AllowAnonymous();
|
||||||
|
Post("/api/users/login-with-google");
|
||||||
|
Options(o => o.WithTags("Users"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(
|
||||||
|
LoginWithGoogleRequest request,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var googleToken = JsonSerializer.Deserialize<GoogleToken>(request.Token)!;
|
||||||
|
|
||||||
|
// Verify the token with Google
|
||||||
|
using var httpClient = httpClientFactory.CreateClient();
|
||||||
|
var response = await httpClient.GetAsync(
|
||||||
|
$"https://www.googleapis.com/oauth2/v1/userinfo?access_token={googleToken.AccessToken}",
|
||||||
|
ct);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
await SendStringAsync(
|
||||||
|
"The token is not valid",
|
||||||
|
400,
|
||||||
|
cancellation: ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the user info (email, name, etc.).
|
||||||
|
var content = await response.Content.ReadAsStringAsync(ct);
|
||||||
|
var userInfo = JsonSerializer.Deserialize<GoogleUserInfo>(content);
|
||||||
|
if (userInfo is null
|
||||||
|
|| !userInfo.VerifiedEmail
|
||||||
|
|| string.IsNullOrEmpty(userInfo.Email))
|
||||||
|
{
|
||||||
|
await SendStringAsync(
|
||||||
|
"The token does not contain an email",
|
||||||
|
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(8, 10);
|
||||||
|
var generatedUser = new ApplicationUser
|
||||||
|
{
|
||||||
|
UserName = userInfo.Email,
|
||||||
|
Email = userInfo.Email,
|
||||||
|
Firstname = userInfo.GivenName,
|
||||||
|
Lastname = userInfo.FamilyName,
|
||||||
|
Alias = userInfo.Name,
|
||||||
|
PortraitUrl = userInfo.Picture,
|
||||||
|
GoogleId = userInfo.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 LoginWithGoogleResponse(accessToken, string.Empty),
|
||||||
|
cancellation: ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user