From 5597b98bf49190c0d42e063a60fd712677213542 Mon Sep 17 00:00:00 2001 From: hadeer Date: Sun, 10 Aug 2025 05:10:06 +0300 Subject: [PATCH 01/92] Apply Soft Delete using ITrackable interface --- AskFm/AskFm.API/Program.cs | 2 +- AskFm/AskFm.DAL/AppDbContext.cs | 12 ++++ .../Migrations/AppDbContextModelSnapshot.cs | 64 +++++++++++++++++++ AskFm/AskFm.DAL/Models/Comment.cs | 6 +- AskFm/AskFm.DAL/Models/CommentLike.cs | 6 +- AskFm/AskFm.DAL/Models/Follow.cs | 6 +- AskFm/AskFm.DAL/Models/ITrackable.cs | 9 +++ AskFm/AskFm.DAL/Models/Notification.cs | 6 +- AskFm/AskFm.DAL/Models/SavedThreads.cs | 6 +- AskFm/AskFm.DAL/Models/Thread.cs | 5 +- AskFm/AskFm.DAL/Models/ThreadLike.cs | 6 +- AskFm/AskFm.DAL/Models/User.cs | 8 ++- 12 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 AskFm/AskFm.DAL/Models/ITrackable.cs diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 807ebe5..76694a2 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -21,7 +21,7 @@ public static void Main(string[] args) { throw new Exception("Connection string is null"); } - builder.Services.AddDbContext(options => + builder.Services.AddDbContext(options => options.UseSqlServer(ConnectionString)); diff --git a/AskFm/AskFm.DAL/AppDbContext.cs b/AskFm/AskFm.DAL/AppDbContext.cs index 70b80cd..499ded5 100644 --- a/AskFm/AskFm.DAL/AppDbContext.cs +++ b/AskFm/AskFm.DAL/AppDbContext.cs @@ -23,5 +23,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); + + foreach (var entityType in modelBuilder.Model.GetEntityTypes() + .Where(t => typeof(ITrackable).IsAssignableFrom(t.ClrType))) + { + modelBuilder.Entity(entityType.ClrType).Property("DeletedAt") + .HasColumnType("DATETIME"); + + modelBuilder.Entity(entityType.ClrType).Property("IsDeleted") + .HasColumnType("BIT") + .HasDefaultValue(false); + } + } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index 9b959ec..80dfd50 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -38,6 +38,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("datetime"); + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + b.Property("LikeCount") .HasColumnType("int"); @@ -72,6 +80,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("datetime"); + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + b.HasKey("UserId", "CommentId"); b.HasIndex("CommentId"); @@ -90,9 +106,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("datetime2"); + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + b.Property("IsActive") .HasColumnType("bit"); + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + b.HasKey("FollowerId", "FollowedId"); b.HasIndex("FollowedId"); @@ -111,6 +135,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("datetime"); + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + b.Property("ResourceId") .HasColumnType("int"); @@ -139,6 +171,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("UserId") .HasColumnType("int"); + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + b.HasKey("SavedThreadId", "UserId"); b.HasIndex("UserId"); @@ -168,6 +208,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("datetime"); + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + b.Property("QuestionContent") .IsRequired() .HasMaxLength(1000) @@ -200,6 +248,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("datetime"); + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + b.HasKey("ThreadId", "UserId"); b.HasIndex("UserId"); @@ -227,6 +283,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("datetime2"); + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + b.Property("Email") .IsRequired() .HasMaxLength(255) @@ -238,6 +297,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FollowingCount") .HasColumnType("int"); + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + b.Property("LastSeen") .HasColumnType("datetime2"); diff --git a/AskFm/AskFm.DAL/Models/Comment.cs b/AskFm/AskFm.DAL/Models/Comment.cs index b4576ba..0359667 100644 --- a/AskFm/AskFm.DAL/Models/Comment.cs +++ b/AskFm/AskFm.DAL/Models/Comment.cs @@ -1,6 +1,8 @@ +using System.Runtime.InteropServices.JavaScript; + namespace AskFm.DAL.Models; -public class Comment +public class Comment : ITrackable { public int Id { get; set; } public string Content { get; set; } @@ -18,4 +20,6 @@ public class Comment public virtual ICollection? Replies { get; set; } public virtual ICollection? CommentLikes { get; set; } + public bool IsDeleted { get; set; } + public DateTime DeletedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/CommentLike.cs b/AskFm/AskFm.DAL/Models/CommentLike.cs index 1bc9030..cdb5c22 100644 --- a/AskFm/AskFm.DAL/Models/CommentLike.cs +++ b/AskFm/AskFm.DAL/Models/CommentLike.cs @@ -1,10 +1,14 @@ +using System.Runtime.InteropServices.JavaScript; + namespace AskFm.DAL.Models; -public class CommentLike +public class CommentLike : ITrackable { public int CommentId { get; set; } public virtual Comment? Comment { get; set; } public int UserId { get; set; } public virtual User? User { get; set; } public DateTime CreatedAt { get; set; } + public bool IsDeleted { get; set; } + public DateTime DeletedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Follow.cs b/AskFm/AskFm.DAL/Models/Follow.cs index 639b05c..8cfbe0b 100644 --- a/AskFm/AskFm.DAL/Models/Follow.cs +++ b/AskFm/AskFm.DAL/Models/Follow.cs @@ -1,6 +1,8 @@ +using System.Runtime.InteropServices.JavaScript; + namespace AskFm.DAL.Models; -public class Follow +public class Follow : ITrackable { public int FollowerId { get; set; } public virtual User? Follower { get; set; } @@ -11,4 +13,6 @@ public class Follow // is this follow available public bool IsActive { get; set; } = true; + public bool IsDeleted { get; set; } + public DateTime DeletedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/ITrackable.cs b/AskFm/AskFm.DAL/Models/ITrackable.cs new file mode 100644 index 0000000..8f1f498 --- /dev/null +++ b/AskFm/AskFm.DAL/Models/ITrackable.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices.JavaScript; + +namespace AskFm.DAL.Models; + +public interface ITrackable +{ + bool IsDeleted { get; set; } + DateTime DeletedAt { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index 5e1cedf..0c1fdb6 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -1,6 +1,8 @@ +using System.Runtime.InteropServices.JavaScript; + namespace AskFm.DAL.Models; -public class Notification +public class Notification : ITrackable { public GCNotificationStatus Type; public int Id { get; set; } @@ -11,4 +13,6 @@ public class Notification public int ResourceId { get; set; } public string jsonContent { get; set; } public DateTime CreatedAt { get; set; } + public bool IsDeleted { get; set; } + public DateTime DeletedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/SavedThreads.cs b/AskFm/AskFm.DAL/Models/SavedThreads.cs index 01189aa..d878399 100644 --- a/AskFm/AskFm.DAL/Models/SavedThreads.cs +++ b/AskFm/AskFm.DAL/Models/SavedThreads.cs @@ -1,10 +1,14 @@ +using System.Runtime.InteropServices.JavaScript; + namespace AskFm.DAL.Models; -public class SavedThreads +public class SavedThreads : ITrackable { public int SavedThreadId { get; set; } public int UserId { get; set; } public virtual Thread? Thread { get; set; } public virtual User? User { get; set; } + public bool IsDeleted { get; set; } + public DateTime DeletedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Thread.cs b/AskFm/AskFm.DAL/Models/Thread.cs index 019f533..b8d9e49 100644 --- a/AskFm/AskFm.DAL/Models/Thread.cs +++ b/AskFm/AskFm.DAL/Models/Thread.cs @@ -1,7 +1,8 @@ +using System.Runtime.InteropServices.JavaScript; using AskFm.DAL.Enums; namespace AskFm.DAL.Models; -public class Thread +public class Thread : ITrackable { public int Id { get; set; } public string QuestionContent { get; set; } @@ -19,4 +20,6 @@ public class Thread public virtual ICollection? Comments { get; set; } public virtual ICollection? ThreadLikes { get; set; } public virtual ICollection? SavedThreads { get; set; } + public bool IsDeleted { get; set; } + public DateTime DeletedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/ThreadLike.cs b/AskFm/AskFm.DAL/Models/ThreadLike.cs index cbf62c7..cb15cda 100644 --- a/AskFm/AskFm.DAL/Models/ThreadLike.cs +++ b/AskFm/AskFm.DAL/Models/ThreadLike.cs @@ -1,6 +1,8 @@ +using System.Runtime.InteropServices.JavaScript; + namespace AskFm.DAL.Models; -public class ThreadLike +public class ThreadLike : ITrackable { public int ThreadId { get; set; } public virtual Thread? Thread { get; set; } @@ -9,4 +11,6 @@ public class ThreadLike public virtual User? User { get; set; } public DateTime CreatedAt { get; set; } + public bool IsDeleted { get; set; } + public DateTime DeletedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/User.cs b/AskFm/AskFm.DAL/Models/User.cs index 26ebe03..20222d6 100644 --- a/AskFm/AskFm.DAL/Models/User.cs +++ b/AskFm/AskFm.DAL/Models/User.cs @@ -1,6 +1,8 @@ +using System.Runtime.InteropServices.JavaScript; + namespace AskFm.DAL.Models; -public class User +public class User : ITrackable { public int Id { get; set; } public string Name { get; set; } @@ -25,5 +27,7 @@ public class User public virtual ICollection? CommentLikes { get; set; } public virtual ICollection? Notifications { get; set; } public virtual ICollection? SavedThreads { get; set; } - + + public bool IsDeleted { get; set; } + public DateTime DeletedAt { get; set; } } \ No newline at end of file From 8ce0cc1e6e89d19785da7e5ce995928d3a92b30c Mon Sep 17 00:00:00 2001 From: hadeer Date: Sun, 10 Aug 2025 05:10:35 +0300 Subject: [PATCH 02/92] Add Migrations of Soft Delete --- .../20250810015924_Soft_Delete.Designer.cs | 500 ++++++++++++++++++ .../Migrations/20250810015924_Soft_Delete.cs | 195 +++++++ 2 files changed, 695 insertions(+) create mode 100644 AskFm/AskFm.DAL/Migrations/20250810015924_Soft_Delete.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250810015924_Soft_Delete.cs diff --git a/AskFm/AskFm.DAL/Migrations/20250810015924_Soft_Delete.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250810015924_Soft_Delete.Designer.cs new file mode 100644 index 0000000..c118480 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250810015924_Soft_Delete.Designer.cs @@ -0,0 +1,500 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250810015924_Soft_Delete")] + partial class Soft_Delete + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("isRead") + .HasColumnType("bit"); + + b.Property("jsonContent") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(225) + .HasColumnType("nvarchar(225)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId"); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.User", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.User", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.User", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId"); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.User", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250810015924_Soft_Delete.cs b/AskFm/AskFm.DAL/Migrations/20250810015924_Soft_Delete.cs new file mode 100644 index 0000000..42bcab7 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250810015924_Soft_Delete.cs @@ -0,0 +1,195 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class Soft_Delete : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "Users", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Users", + type: "BIT", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "ThreadLikes", + type: "BIT", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Thread", + type: "BIT", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "SavedThreads", + type: "BIT", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Notifications", + type: "BIT", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Follows", + type: "BIT", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Comments", + type: "BIT", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "CommentLikes", + type: "BIT", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "Users"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Users"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "ThreadLikes"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "ThreadLikes"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "Thread"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Thread"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "SavedThreads"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "SavedThreads"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "Notifications"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Notifications"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "Follows"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Follows"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "Comments"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Comments"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "CommentLikes"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "CommentLikes"); + } + } +} From bc95408317a61e6a53796c09f1a11b456ddc32b2 Mon Sep 17 00:00:00 2001 From: hadeer Date: Sun, 10 Aug 2025 05:16:38 +0300 Subject: [PATCH 03/92] Refactor: set all relatoin to No Action due to soft deletion --- ...0021444_set_no_action_OnDelete.Designer.cs | 502 ++++++++++++++++++ .../20250810021444_set_no_action_OnDelete.cs | 133 +++++ .../Migrations/AppDbContextModelSnapshot.cs | 16 +- .../CommentConfigration.cs | 4 +- .../CommentLikeConfigration.cs | 2 +- .../NotificationConfigration.cs | 2 +- .../ModelsConfigrations/ThreadConfigration.cs | 4 +- .../ThreadLikeConfigration.cs | 2 +- 8 files changed, 651 insertions(+), 14 deletions(-) create mode 100644 AskFm/AskFm.DAL/Migrations/20250810021444_set_no_action_OnDelete.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250810021444_set_no_action_OnDelete.cs diff --git a/AskFm/AskFm.DAL/Migrations/20250810021444_set_no_action_OnDelete.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250810021444_set_no_action_OnDelete.Designer.cs new file mode 100644 index 0000000..17a292e --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250810021444_set_no_action_OnDelete.Designer.cs @@ -0,0 +1,502 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250810021444_set_no_action_OnDelete")] + partial class set_no_action_OnDelete + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("isRead") + .HasColumnType("bit"); + + b.Property("jsonContent") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(225) + .HasColumnType("nvarchar(225)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.User", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.User", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.User", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.User", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.User", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250810021444_set_no_action_OnDelete.cs b/AskFm/AskFm.DAL/Migrations/20250810021444_set_no_action_OnDelete.cs new file mode 100644 index 0000000..9565fab --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250810021444_set_no_action_OnDelete.cs @@ -0,0 +1,133 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class set_no_action_OnDelete : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CommentLikes_Users_UserId", + table: "CommentLikes"); + + migrationBuilder.DropForeignKey( + name: "FK_Comments_Thread_ThreadId", + table: "Comments"); + + migrationBuilder.DropForeignKey( + name: "FK_Notifications_Users_UserId", + table: "Notifications"); + + migrationBuilder.DropForeignKey( + name: "FK_Thread_Users_AskedId", + table: "Thread"); + + migrationBuilder.DropForeignKey( + name: "FK_ThreadLikes_Users_UserId", + table: "ThreadLikes"); + + migrationBuilder.AddForeignKey( + name: "FK_CommentLikes_Users_UserId", + table: "CommentLikes", + column: "UserId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Comments_Thread_ThreadId", + table: "Comments", + column: "ThreadId", + principalTable: "Thread", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Notifications_Users_UserId", + table: "Notifications", + column: "UserId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Thread_Users_AskedId", + table: "Thread", + column: "AskedId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ThreadLikes_Users_UserId", + table: "ThreadLikes", + column: "UserId", + principalTable: "Users", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CommentLikes_Users_UserId", + table: "CommentLikes"); + + migrationBuilder.DropForeignKey( + name: "FK_Comments_Thread_ThreadId", + table: "Comments"); + + migrationBuilder.DropForeignKey( + name: "FK_Notifications_Users_UserId", + table: "Notifications"); + + migrationBuilder.DropForeignKey( + name: "FK_Thread_Users_AskedId", + table: "Thread"); + + migrationBuilder.DropForeignKey( + name: "FK_ThreadLikes_Users_UserId", + table: "ThreadLikes"); + + migrationBuilder.AddForeignKey( + name: "FK_CommentLikes_Users_UserId", + table: "CommentLikes", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Comments_Thread_ThreadId", + table: "Comments", + column: "ThreadId", + principalTable: "Thread", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Notifications_Users_UserId", + table: "Notifications", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Thread_Users_AskedId", + table: "Thread", + column: "AskedId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ThreadLikes_Users_UserId", + table: "ThreadLikes", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index 80dfd50..8f3579e 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -331,12 +331,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") .WithMany("Replies") - .HasForeignKey("ParentCommentId"); + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); b.HasOne("AskFm.DAL.Models.Thread", "Thread") .WithMany("Comments") .HasForeignKey("ThreadId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.NoAction) .IsRequired(); b.HasOne("AskFm.DAL.Models.User", "User") @@ -362,7 +363,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("AskFm.DAL.Models.User", "User") .WithMany("CommentLikes") .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.NoAction) .IsRequired(); b.Navigation("Comment"); @@ -394,7 +395,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("AskFm.DAL.Models.User", "User") .WithMany("Notifications") .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.NoAction) .IsRequired(); b.Navigation("User"); @@ -424,12 +425,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("AskFm.DAL.Models.User", "Asked") .WithMany("ReceivedThreads") .HasForeignKey("AskedId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.NoAction) .IsRequired(); b.HasOne("AskFm.DAL.Models.User", "Asker") .WithMany("AskedThreads") - .HasForeignKey("AskerId"); + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); b.Navigation("Asked"); @@ -447,7 +449,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("AskFm.DAL.Models.User", "User") .WithMany("ThreadLikes") .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.NoAction) .IsRequired(); b.Navigation("Thread"); diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs index 0bc7fd6..97e7bd7 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs @@ -22,12 +22,12 @@ public void Configure(EntityTypeBuilder builder) builder.HasOne(c => c.Thread) .WithMany(t => t.Comments) .HasForeignKey(c => c.ThreadId) - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.NoAction); builder.HasOne(c => c.ParentComment) .WithMany(c => c.Replies) .HasForeignKey(c => c.ParentCommentId) - .OnDelete(DeleteBehavior.ClientSetNull); + .OnDelete(DeleteBehavior.NoAction); builder.HasOne(c => c.User) diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/CommentLikeConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/CommentLikeConfigration.cs index a7e29dc..0de2c89 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/CommentLikeConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/CommentLikeConfigration.cs @@ -17,7 +17,7 @@ public void Configure(EntityTypeBuilder builder) builder.HasOne(cl => cl.User) .WithMany(u => u.CommentLikes) .HasForeignKey(cl => cl.UserId) - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.NoAction); builder.HasOne(cl => cl.Comment) .WithMany(c => c.CommentLikes) diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs index 4c11b35..1894dae 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs @@ -23,6 +23,6 @@ public void Configure(EntityTypeBuilder builder) builder.HasOne(n => n.User) .WithMany(u => u.Notifications) .HasForeignKey(n => n.UserId) - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.NoAction); } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/ThreadConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/ThreadConfigration.cs index 0f9cdb8..2297076 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/ThreadConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/ThreadConfigration.cs @@ -32,12 +32,12 @@ public void Configure(EntityTypeBuilder builder) builder.HasOne(t => t.Asked) .WithMany(u => u.ReceivedThreads) .HasForeignKey(t => t.AskedId) - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.NoAction); builder.HasOne(t => t.Asker) .WithMany(u => u.AskedThreads) .HasForeignKey(t => t.AskerId) - .OnDelete(DeleteBehavior.ClientSetNull); + .OnDelete(DeleteBehavior.NoAction); diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/ThreadLikeConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/ThreadLikeConfigration.cs index a1ef8f6..19646be 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/ThreadLikeConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/ThreadLikeConfigration.cs @@ -17,7 +17,7 @@ public void Configure(EntityTypeBuilder builder) builder.HasOne(tl => tl.User) .WithMany(u => u.ThreadLikes) .HasForeignKey(tl => tl.UserId) - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.NoAction); builder.HasOne(tl => tl.Thread) .WithMany(t => t.ThreadLikes) From 279df67346b4194a0878ed68750e0d624f13f0c7 Mon Sep 17 00:00:00 2001 From: hadeer Date: Sun, 10 Aug 2025 06:46:59 +0300 Subject: [PATCH 04/92] Feat: Integrate with Identity user for authentication & authrization and update database --- AskFm/AskFm.API/AskFm.API.csproj | 1 + AskFm/AskFm.API/Program.cs | 7 + AskFm/AskFm.DAL/AppDbContext.cs | 7 +- AskFm/AskFm.DAL/AskFm.DAL.csproj | 1 + .../20250810034350_using_identity.Designer.cs | 728 ++++++++++++++++++ .../20250810034350_using_identity.cs | 641 +++++++++++++++ .../Migrations/AppDbContextModelSnapshot.cs | 332 ++++++-- .../Models/{User.cs => ApplicationUser.cs} | 6 +- AskFm/AskFm.DAL/Models/Comment.cs | 3 +- AskFm/AskFm.DAL/Models/CommentLike.cs | 2 +- AskFm/AskFm.DAL/Models/Follow.cs | 4 +- AskFm/AskFm.DAL/Models/Notification.cs | 2 +- AskFm/AskFm.DAL/Models/SavedThreads.cs | 2 +- AskFm/AskFm.DAL/Models/Thread.cs | 4 +- AskFm/AskFm.DAL/Models/ThreadLike.cs | 2 +- ...tion.cs => ApplicationUserConfigration.cs} | 11 +- 16 files changed, 1679 insertions(+), 74 deletions(-) create mode 100644 AskFm/AskFm.DAL/Migrations/20250810034350_using_identity.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250810034350_using_identity.cs rename AskFm/AskFm.DAL/Models/{User.cs => ApplicationUser.cs} (88%) rename AskFm/AskFm.DAL/ModelsConfigrations/{UserConfigration.cs => ApplicationUserConfigration.cs} (60%) diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index d791ad5..61a4c80 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -8,6 +8,7 @@ + diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 76694a2..4f74e1d 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -1,8 +1,11 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; using AskFm.DAL; +using AskFm.DAL.Models; using DotNetEnv; namespace AskFm.API; + public class Program { public static void Main(string[] args) @@ -23,6 +26,10 @@ public static void Main(string[] args) } builder.Services.AddDbContext(options => options.UseSqlServer(ConnectionString)); + + builder.Services.AddIdentity>() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); var app = builder.Build(); diff --git a/AskFm/AskFm.DAL/AppDbContext.cs b/AskFm/AskFm.DAL/AppDbContext.cs index 499ded5..557e5f4 100644 --- a/AskFm/AskFm.DAL/AppDbContext.cs +++ b/AskFm/AskFm.DAL/AppDbContext.cs @@ -1,16 +1,19 @@ +using System.Data; using AskFm.DAL.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Thread = AskFm.DAL.Models.Thread; namespace AskFm.DAL; -public class AppDbContext : DbContext +public class AppDbContext : IdentityDbContext, int> { public AppDbContext(DbContextOptions options) : base(options) { } - public DbSet Users { get; set; } + public DbSet Users { get; set; } public DbSet Threads { get; set; } public DbSet Comments { get; set; } public DbSet Follows { get; set; } diff --git a/AskFm/AskFm.DAL/AskFm.DAL.csproj b/AskFm/AskFm.DAL/AskFm.DAL.csproj index 4b32852..c77d955 100644 --- a/AskFm/AskFm.DAL/AskFm.DAL.csproj +++ b/AskFm/AskFm.DAL/AskFm.DAL.csproj @@ -8,6 +8,7 @@ + all diff --git a/AskFm/AskFm.DAL/Migrations/20250810034350_using_identity.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250810034350_using_identity.Designer.cs new file mode 100644 index 0000000..1612fcc --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250810034350_using_identity.Designer.cs @@ -0,0 +1,728 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250810034350_using_identity")] + partial class using_identity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("isRead") + .HasColumnType("bit"); + + b.Property("jsonContent") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250810034350_using_identity.cs b/AskFm/AskFm.DAL/Migrations/20250810034350_using_identity.cs new file mode 100644 index 0000000..6308ec5 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250810034350_using_identity.cs @@ -0,0 +1,641 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class using_identity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CommentLikes_Users_UserId", + table: "CommentLikes"); + + migrationBuilder.DropForeignKey( + name: "FK_Comments_Users_UserId", + table: "Comments"); + + migrationBuilder.DropForeignKey( + name: "FK_Follows_Users_FollowedId", + table: "Follows"); + + migrationBuilder.DropForeignKey( + name: "FK_Follows_Users_FollowerId", + table: "Follows"); + + migrationBuilder.DropForeignKey( + name: "FK_Notifications_Users_UserId", + table: "Notifications"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedThreads_Users_UserId", + table: "SavedThreads"); + + migrationBuilder.DropForeignKey( + name: "FK_Thread_Users_AskedId", + table: "Thread"); + + migrationBuilder.DropForeignKey( + name: "FK_Thread_Users_AskerId", + table: "Thread"); + + migrationBuilder.DropForeignKey( + name: "FK_ThreadLikes_Users_UserId", + table: "ThreadLikes"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Users", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Password", + table: "Users"); + + migrationBuilder.RenameTable( + name: "Users", + newName: "AspNetUsers"); + + migrationBuilder.RenameColumn( + name: "Username", + table: "AspNetUsers", + newName: "UserName"); + + migrationBuilder.RenameIndex( + name: "IX_Users_Username", + table: "AspNetUsers", + newName: "IX_AspNetUsers_UserName"); + + migrationBuilder.AlterColumn( + name: "UserName", + table: "AspNetUsers", + type: "nvarchar(256)", + maxLength: 256, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "IsDeleted", + table: "AspNetUsers", + type: "bit", + nullable: false, + oldClrType: typeof(bool), + oldType: "BIT", + oldDefaultValue: false); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AddColumn( + name: "AccessFailedCount", + table: "AspNetUsers", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ConcurrencyStamp", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "EmailConfirmed", + table: "AspNetUsers", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LockoutEnabled", + table: "AspNetUsers", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LockoutEnd", + table: "AspNetUsers", + type: "datetimeoffset", + nullable: true); + + migrationBuilder.AddColumn( + name: "NormalizedEmail", + table: "AspNetUsers", + type: "nvarchar(256)", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "NormalizedUserName", + table: "AspNetUsers", + type: "nvarchar(256)", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "PasswordHash", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "PhoneNumber", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "PhoneNumberConfirmed", + table: "AspNetUsers", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SecurityStamp", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "TwoFactorEnabled", + table: "AspNetUsers", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddPrimaryKey( + name: "PK_AspNetUsers", + table: "AspNetUsers", + column: "Id"); + + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "int", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "int", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "int", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "int", nullable: false), + RoleId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.AddForeignKey( + name: "FK_CommentLikes_AspNetUsers_UserId", + table: "CommentLikes", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Comments_AspNetUsers_UserId", + table: "Comments", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Follows_AspNetUsers_FollowedId", + table: "Follows", + column: "FollowedId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Follows_AspNetUsers_FollowerId", + table: "Follows", + column: "FollowerId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Notifications_AspNetUsers_UserId", + table: "Notifications", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_SavedThreads_AspNetUsers_UserId", + table: "SavedThreads", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Thread_AspNetUsers_AskedId", + table: "Thread", + column: "AskedId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Thread_AspNetUsers_AskerId", + table: "Thread", + column: "AskerId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ThreadLikes_AspNetUsers_UserId", + table: "ThreadLikes", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CommentLikes_AspNetUsers_UserId", + table: "CommentLikes"); + + migrationBuilder.DropForeignKey( + name: "FK_Comments_AspNetUsers_UserId", + table: "Comments"); + + migrationBuilder.DropForeignKey( + name: "FK_Follows_AspNetUsers_FollowedId", + table: "Follows"); + + migrationBuilder.DropForeignKey( + name: "FK_Follows_AspNetUsers_FollowerId", + table: "Follows"); + + migrationBuilder.DropForeignKey( + name: "FK_Notifications_AspNetUsers_UserId", + table: "Notifications"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedThreads_AspNetUsers_UserId", + table: "SavedThreads"); + + migrationBuilder.DropForeignKey( + name: "FK_Thread_AspNetUsers_AskedId", + table: "Thread"); + + migrationBuilder.DropForeignKey( + name: "FK_Thread_AspNetUsers_AskerId", + table: "Thread"); + + migrationBuilder.DropForeignKey( + name: "FK_ThreadLikes_AspNetUsers_UserId", + table: "ThreadLikes"); + + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropPrimaryKey( + name: "PK_AspNetUsers", + table: "AspNetUsers"); + + migrationBuilder.DropIndex( + name: "EmailIndex", + table: "AspNetUsers"); + + migrationBuilder.DropIndex( + name: "UserNameIndex", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "AccessFailedCount", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "ConcurrencyStamp", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "EmailConfirmed", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "LockoutEnabled", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "LockoutEnd", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "NormalizedEmail", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "NormalizedUserName", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "PasswordHash", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "PhoneNumber", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "PhoneNumberConfirmed", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "SecurityStamp", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "TwoFactorEnabled", + table: "AspNetUsers"); + + migrationBuilder.RenameTable( + name: "AspNetUsers", + newName: "Users"); + + migrationBuilder.RenameColumn( + name: "UserName", + table: "Users", + newName: "Username"); + + migrationBuilder.RenameIndex( + name: "IX_AspNetUsers_UserName", + table: "Users", + newName: "IX_Users_Username"); + + migrationBuilder.AlterColumn( + name: "Username", + table: "Users", + type: "nvarchar(450)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(256)", + oldMaxLength: 256); + + migrationBuilder.AlterColumn( + name: "IsDeleted", + table: "Users", + type: "BIT", + nullable: false, + defaultValue: false, + oldClrType: typeof(bool), + oldType: "bit"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Users", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2"); + + migrationBuilder.AddColumn( + name: "Password", + table: "Users", + type: "nvarchar(225)", + maxLength: 225, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddPrimaryKey( + name: "PK_Users", + table: "Users", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_CommentLikes_Users_UserId", + table: "CommentLikes", + column: "UserId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Comments_Users_UserId", + table: "Comments", + column: "UserId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Follows_Users_FollowedId", + table: "Follows", + column: "FollowedId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Follows_Users_FollowerId", + table: "Follows", + column: "FollowerId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Notifications_Users_UserId", + table: "Notifications", + column: "UserId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_SavedThreads_Users_UserId", + table: "SavedThreads", + column: "UserId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Thread_Users_AskedId", + table: "Thread", + column: "AskedId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Thread_Users_AskerId", + table: "Thread", + column: "AskerId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ThreadLikes_Users_UserId", + table: "ThreadLikes", + column: "UserId", + principalTable: "Users", + principalColumn: "Id"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index 8f3579e..34f3467 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -22,6 +22,112 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => { b.Property("Id") @@ -263,7 +369,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ThreadLikes"); }); - modelBuilder.Entity("AskFm.DAL.Models.User", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -271,60 +377,129 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("AvatarPath") - .IsRequired() + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() .HasColumnType("nvarchar(max)"); - b.Property("Bio") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("nvarchar(500)"); + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); - b.Property("CreatedAt") - .HasColumnType("datetime2"); + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); - b.Property("DeletedAt") - .HasColumnType("DATETIME"); + b.HasKey("Id"); - b.Property("Email") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("nvarchar(255)"); + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); - b.Property("FollowersCount") + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("int"); - b.Property("FollowingCount") + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") .HasColumnType("int"); - b.Property("IsDeleted") + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("BIT") - .HasDefaultValue(false); + .HasColumnType("int"); - b.Property("LastSeen") - .HasColumnType("datetime2"); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); - b.Property("Password") - .IsRequired() - .HasMaxLength(225) - .HasColumnType("nvarchar(225)"); + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); - b.Property("Username") - .IsRequired() - .HasColumnType("nvarchar(450)"); + b.Property("UserId") + .HasColumnType("int"); b.HasKey("Id"); - b.HasIndex("Username") - .IsUnique(); + b.HasIndex("UserId"); - b.ToTable("Users"); + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); }); modelBuilder.Entity("AskFm.DAL.Models.Comment", b => @@ -340,7 +515,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.NoAction) .IsRequired(); - b.HasOne("AskFm.DAL.Models.User", "User") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") .WithMany("Comments") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.NoAction); @@ -360,7 +535,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.NoAction) .IsRequired(); - b.HasOne("AskFm.DAL.Models.User", "User") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") .WithMany("CommentLikes") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.NoAction) @@ -373,13 +548,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("AskFm.DAL.Models.Follow", b => { - b.HasOne("AskFm.DAL.Models.User", "Followed") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") .WithMany("Followers") .HasForeignKey("FollowedId") .OnDelete(DeleteBehavior.NoAction) .IsRequired(); - b.HasOne("AskFm.DAL.Models.User", "Follower") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") .WithMany("Following") .HasForeignKey("FollowerId") .OnDelete(DeleteBehavior.NoAction) @@ -392,7 +567,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("AskFm.DAL.Models.Notification", b => { - b.HasOne("AskFm.DAL.Models.User", "User") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") .WithMany("Notifications") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.NoAction) @@ -409,7 +584,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.NoAction) .IsRequired(); - b.HasOne("AskFm.DAL.Models.User", "User") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") .WithMany("SavedThreads") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.NoAction) @@ -422,13 +597,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("AskFm.DAL.Models.Thread", b => { - b.HasOne("AskFm.DAL.Models.User", "Asked") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") .WithMany("ReceivedThreads") .HasForeignKey("AskedId") .OnDelete(DeleteBehavior.NoAction) .IsRequired(); - b.HasOne("AskFm.DAL.Models.User", "Asker") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") .WithMany("AskedThreads") .HasForeignKey("AskerId") .OnDelete(DeleteBehavior.NoAction); @@ -446,7 +621,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.NoAction) .IsRequired(); - b.HasOne("AskFm.DAL.Models.User", "User") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") .WithMany("ThreadLikes") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.NoAction) @@ -457,23 +632,58 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { - b.Navigation("CommentLikes"); + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); - b.Navigation("Replies"); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); - modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.Navigation("Comments"); + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); - b.Navigation("SavedThreads"); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("ThreadLikes"); + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); - modelBuilder.Entity("AskFm.DAL.Models.User", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => { b.Navigation("AskedThreads"); @@ -491,6 +701,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("SavedThreads"); + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + b.Navigation("ThreadLikes"); }); #pragma warning restore 612, 618 diff --git a/AskFm/AskFm.DAL/Models/User.cs b/AskFm/AskFm.DAL/Models/ApplicationUser.cs similarity index 88% rename from AskFm/AskFm.DAL/Models/User.cs rename to AskFm/AskFm.DAL/Models/ApplicationUser.cs index 20222d6..757b7ff 100644 --- a/AskFm/AskFm.DAL/Models/User.cs +++ b/AskFm/AskFm.DAL/Models/ApplicationUser.cs @@ -1,14 +1,12 @@ using System.Runtime.InteropServices.JavaScript; +using Microsoft.AspNetCore.Identity; namespace AskFm.DAL.Models; -public class User : ITrackable +public class ApplicationUser : IdentityUser { - public int Id { get; set; } public string Name { get; set; } - public string Username { get; set; } public string Email { get; set; } - public string Password { get; set; } public string Bio { get; set; } public string AvatarPath { get; set; } diff --git a/AskFm/AskFm.DAL/Models/Comment.cs b/AskFm/AskFm.DAL/Models/Comment.cs index 0359667..3a53aeb 100644 --- a/AskFm/AskFm.DAL/Models/Comment.cs +++ b/AskFm/AskFm.DAL/Models/Comment.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices.JavaScript; +using Microsoft.AspNetCore.Identity; namespace AskFm.DAL.Models; @@ -10,7 +11,7 @@ public class Comment : ITrackable public int LikeCount { get; set; } public int? UserId { get; set; } - public virtual User? User { get; set; } + public virtual ApplicationUser? User { get; set; } public int ThreadId { get; set; } public virtual Thread? Thread { get; set; } diff --git a/AskFm/AskFm.DAL/Models/CommentLike.cs b/AskFm/AskFm.DAL/Models/CommentLike.cs index cdb5c22..cd49b27 100644 --- a/AskFm/AskFm.DAL/Models/CommentLike.cs +++ b/AskFm/AskFm.DAL/Models/CommentLike.cs @@ -7,7 +7,7 @@ public class CommentLike : ITrackable public int CommentId { get; set; } public virtual Comment? Comment { get; set; } public int UserId { get; set; } - public virtual User? User { get; set; } + public virtual ApplicationUser? User { get; set; } public DateTime CreatedAt { get; set; } public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } diff --git a/AskFm/AskFm.DAL/Models/Follow.cs b/AskFm/AskFm.DAL/Models/Follow.cs index 8cfbe0b..1bccfe6 100644 --- a/AskFm/AskFm.DAL/Models/Follow.cs +++ b/AskFm/AskFm.DAL/Models/Follow.cs @@ -5,10 +5,10 @@ namespace AskFm.DAL.Models; public class Follow : ITrackable { public int FollowerId { get; set; } - public virtual User? Follower { get; set; } + public virtual ApplicationUser? Follower { get; set; } public int FollowedId { get; set; } - public virtual User? Followed { get; set; } + public virtual ApplicationUser? Followed { get; set; } public DateTime CreatedAt { get; set; } // is this follow available diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index 0c1fdb6..c4eda71 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -7,7 +7,7 @@ public class Notification : ITrackable public GCNotificationStatus Type; public int Id { get; set; } public int UserId { get; set; } - public virtual User? User { get; set; } + public virtual ApplicationUser? User { get; set; } public bool isRead { get; set; } public int ResourceId { get; set; } diff --git a/AskFm/AskFm.DAL/Models/SavedThreads.cs b/AskFm/AskFm.DAL/Models/SavedThreads.cs index d878399..95daf5a 100644 --- a/AskFm/AskFm.DAL/Models/SavedThreads.cs +++ b/AskFm/AskFm.DAL/Models/SavedThreads.cs @@ -8,7 +8,7 @@ public class SavedThreads : ITrackable public int UserId { get; set; } public virtual Thread? Thread { get; set; } - public virtual User? User { get; set; } + public virtual ApplicationUser? User { get; set; } public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Thread.cs b/AskFm/AskFm.DAL/Models/Thread.cs index b8d9e49..86c29bd 100644 --- a/AskFm/AskFm.DAL/Models/Thread.cs +++ b/AskFm/AskFm.DAL/Models/Thread.cs @@ -12,10 +12,10 @@ public class Thread : ITrackable public bool isAnonymous { get; set; } public DateTime CreatedAt { get; set; } public int? AskerId { get; set; } - public virtual User? Asker { get; set; } + public virtual ApplicationUser? Asker { get; set; } public int AskedId { get; set; } - public virtual User? Asked { get; set; } + public virtual ApplicationUser? Asked { get; set; } public virtual ICollection? Comments { get; set; } public virtual ICollection? ThreadLikes { get; set; } diff --git a/AskFm/AskFm.DAL/Models/ThreadLike.cs b/AskFm/AskFm.DAL/Models/ThreadLike.cs index cb15cda..f185c71 100644 --- a/AskFm/AskFm.DAL/Models/ThreadLike.cs +++ b/AskFm/AskFm.DAL/Models/ThreadLike.cs @@ -8,7 +8,7 @@ public class ThreadLike : ITrackable public virtual Thread? Thread { get; set; } public int UserId { get; set; } - public virtual User? User { get; set; } + public virtual ApplicationUser? User { get; set; } public DateTime CreatedAt { get; set; } public bool IsDeleted { get; set; } diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/UserConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/ApplicationUserConfigration.cs similarity index 60% rename from AskFm/AskFm.DAL/ModelsConfigrations/UserConfigration.cs rename to AskFm/AskFm.DAL/ModelsConfigrations/ApplicationUserConfigration.cs index b549241..27b5010 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/UserConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/ApplicationUserConfigration.cs @@ -3,12 +3,12 @@ namespace AskFm.DAL.Models; -public class UserConfigration : IEntityTypeConfiguration +public class ApplicationUserConfigration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { builder.HasKey(x => x.Id); - builder.HasIndex(u => u.Username).IsUnique(); + builder.HasIndex(u => u.UserName).IsUnique(); builder.Property(u => u.Name) .HasMaxLength(50) @@ -17,10 +17,9 @@ public void Configure(EntityTypeBuilder builder) builder.Property(u => u.Bio) .HasMaxLength(500); - builder.Property(x => x.Username).IsRequired(); + builder.Property(x => x.UserName).IsRequired(); - builder.Property(x => x.Password) - .HasMaxLength(225) + builder.Property(x => x.PasswordHash) .IsRequired(); builder.Property(x => x.Email) From 11aed4daa9c81fc481588f0e83665827094700ca Mon Sep 17 00:00:00 2001 From: hadeer Date: Sun, 10 Aug 2025 07:04:27 +0300 Subject: [PATCH 05/92] make code rabbit review in all branches and PRs --- AskFm/.coderabbit.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 AskFm/.coderabbit.yaml diff --git a/AskFm/.coderabbit.yaml b/AskFm/.coderabbit.yaml new file mode 100644 index 0000000..650c4d9 --- /dev/null +++ b/AskFm/.coderabbit.yaml @@ -0,0 +1,7 @@ +reviews: + auto_review: + enabled: true + branches: + - "*" + + dismiss_stale_reviews: true From cde3ad6051768fc53044fed5acd0e3f7f2a94dc8 Mon Sep 17 00:00:00 2001 From: Hadeer Ramadan <147225434+hadeer-r@users.noreply.github.com> Date: Sun, 10 Aug 2025 07:15:28 +0300 Subject: [PATCH 06/92] Create .coderabbit.yaml --- .coderabbit.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..650c4d9 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,7 @@ +reviews: + auto_review: + enabled: true + branches: + - "*" + + dismiss_stale_reviews: true From 799627b22d39bbf1c44c4809b3ef32fd71e506c7 Mon Sep 17 00:00:00 2001 From: hadeer Date: Sun, 10 Aug 2025 07:17:11 +0300 Subject: [PATCH 07/92] remove .coderabbit --- AskFm/.coderabbit.yaml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 AskFm/.coderabbit.yaml diff --git a/AskFm/.coderabbit.yaml b/AskFm/.coderabbit.yaml deleted file mode 100644 index 650c4d9..0000000 --- a/AskFm/.coderabbit.yaml +++ /dev/null @@ -1,7 +0,0 @@ -reviews: - auto_review: - enabled: true - branches: - - "*" - - dismiss_stale_reviews: true From 322af9f53111c61af989212f8692afd765189807 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sun, 10 Aug 2025 19:09:37 +0300 Subject: [PATCH 08/92] Add: - IRepository Interfaces - Repository implementation (Ripository.cs) - IUnitOfWork interface and UnitOfWork class to apply the "Unit of Work pattern" - Repository interface for each model to handle the future special methods. TODO: - Delete unnecessary files "Model's repository" if there is not special methodes needed. - Think again about other potential methods to add at IRepository interface e.g (Count, overloading methods for Find and FindAll, ... etc) - Think on the IEnumerable & IQueyable problem --- AskFm/AskFm.API/AskFm.API.csproj | 1 + AskFm/AskFm.API/Program.cs | 9 +- .../Interfaces/ICommentLikeRepository.cs | 6 + .../Interfaces/ICommentRepository.cs | 6 + .../AskFm.DAL/Interfaces/IFollowRepository.cs | 6 + .../Interfaces/INotificationRepository.cs | 6 + AskFm/AskFm.DAL/Interfaces/IRepository.cs | 36 ++++++ .../Interfaces/ISavedThreadsRepository.cs | 6 + .../Interfaces/IThreadLikeRepository.cs | 6 + .../AskFm.DAL/Interfaces/IThreadRepository.cs | 6 + AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs | 20 +++ AskFm/AskFm.DAL/Interfaces/IUserRepository.cs | 9 ++ .../Repositories/CommentLikeRepository.cs | 6 + .../Repositories/CommentRepository.cs | 6 + .../Repositories/FollowRepository.cs | 6 + .../Repositories/NotificationRepository.cs | 6 + AskFm/AskFm.DAL/Repositories/Repository.cs | 114 ++++++++++++++++++ .../Repositories/SavedThreadRepository.cs | 6 + .../Repositories/ThreadLikeRepository.cs | 6 + .../Repositories/ThreadRepository.cs | 6 + .../AskFm.DAL/Repositories/UserRepository.cs | 19 +++ AskFm/AskFm.DAL/UnitOfWork.cs | 52 ++++++++ 22 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 AskFm/AskFm.DAL/Interfaces/ICommentLikeRepository.cs create mode 100644 AskFm/AskFm.DAL/Interfaces/ICommentRepository.cs create mode 100644 AskFm/AskFm.DAL/Interfaces/IFollowRepository.cs create mode 100644 AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs create mode 100644 AskFm/AskFm.DAL/Interfaces/IRepository.cs create mode 100644 AskFm/AskFm.DAL/Interfaces/ISavedThreadsRepository.cs create mode 100644 AskFm/AskFm.DAL/Interfaces/IThreadLikeRepository.cs create mode 100644 AskFm/AskFm.DAL/Interfaces/IThreadRepository.cs create mode 100644 AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs create mode 100644 AskFm/AskFm.DAL/Interfaces/IUserRepository.cs create mode 100644 AskFm/AskFm.DAL/Repositories/CommentLikeRepository.cs create mode 100644 AskFm/AskFm.DAL/Repositories/CommentRepository.cs create mode 100644 AskFm/AskFm.DAL/Repositories/FollowRepository.cs create mode 100644 AskFm/AskFm.DAL/Repositories/NotificationRepository.cs create mode 100644 AskFm/AskFm.DAL/Repositories/Repository.cs create mode 100644 AskFm/AskFm.DAL/Repositories/SavedThreadRepository.cs create mode 100644 AskFm/AskFm.DAL/Repositories/ThreadLikeRepository.cs create mode 100644 AskFm/AskFm.DAL/Repositories/ThreadRepository.cs create mode 100644 AskFm/AskFm.DAL/Repositories/UserRepository.cs create mode 100644 AskFm/AskFm.DAL/UnitOfWork.cs diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index d791ad5..2c79a8c 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -14,6 +14,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 807ebe5..abf9cf2 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -1,6 +1,9 @@ using Microsoft.EntityFrameworkCore; using AskFm.DAL; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Repositories; using DotNetEnv; +using Microsoft.EntityFrameworkCore.Proxies; namespace AskFm.API; public class Program @@ -22,7 +25,11 @@ public static void Main(string[] args) throw new Exception("Connection string is null"); } builder.Services.AddDbContext(options => - options.UseSqlServer(ConnectionString)); + options + .UseLazyLoadingProxies() + .UseSqlServer(ConnectionString)); + + builder.Services.AddTransient(); var app = builder.Build(); diff --git a/AskFm/AskFm.DAL/Interfaces/ICommentLikeRepository.cs b/AskFm/AskFm.DAL/Interfaces/ICommentLikeRepository.cs new file mode 100644 index 0000000..589ca2a --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/ICommentLikeRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Interfaces; + +public interface ICommentLikeRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/ICommentRepository.cs b/AskFm/AskFm.DAL/Interfaces/ICommentRepository.cs new file mode 100644 index 0000000..e73360d --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/ICommentRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Interfaces; + +public interface ICommentRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IFollowRepository.cs b/AskFm/AskFm.DAL/Interfaces/IFollowRepository.cs new file mode 100644 index 0000000..bfb20b8 --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/IFollowRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Interfaces; + +public interface IFollowRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs new file mode 100644 index 0000000..d2883fd --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Interfaces; + +public interface INotificationRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IRepository.cs b/AskFm/AskFm.DAL/Interfaces/IRepository.cs new file mode 100644 index 0000000..27b6962 --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/IRepository.cs @@ -0,0 +1,36 @@ +using System.Linq.Expressions; + +namespace AskFm.DAL.Interfaces; + +public interface IRepository where T : class +{ + IQueryable GetAll(); + Task> GetAllAsync(); + + + T? GetById(int id); + Task GetByIdAsync(int id); + + + IQueryable FindAll(Expression> predicate, string[] includes = null); + Task> FindAllAsync(Expression> predicate = null, string[] includes = null); + + + + T? Find(Expression> predicate, string[] includes = null); + Task FindAsync(Expression> predicate, string[] includes = null); + + + void Add(T entity); + Task AddAsync(T entity); + + + void Update(T entity); + Task UpdateAsync(T entity); + + + void Remove(T entity); + Task RemoveAsync(T entity); + + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/ISavedThreadsRepository.cs b/AskFm/AskFm.DAL/Interfaces/ISavedThreadsRepository.cs new file mode 100644 index 0000000..960219c --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/ISavedThreadsRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Interfaces; + +public interface ISavedThreadsRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IThreadLikeRepository.cs b/AskFm/AskFm.DAL/Interfaces/IThreadLikeRepository.cs new file mode 100644 index 0000000..254d64e --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/IThreadLikeRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Interfaces; + +public interface IThreadLikeRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IThreadRepository.cs b/AskFm/AskFm.DAL/Interfaces/IThreadRepository.cs new file mode 100644 index 0000000..c5c4aa0 --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/IThreadRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Interfaces; + +public interface IThreadRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs new file mode 100644 index 0000000..11871b2 --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs @@ -0,0 +1,20 @@ +using AskFm.DAL.Models; +using Thread = System.Threading.Thread; + +namespace AskFm.DAL.Interfaces; + +public interface IUnitOfWork : IDisposable +{ + IRepository Users { get; } + IRepository Threads { get; } + IRepository SavedThreads { get; } + IRepository ThreadLikes { get; } + IRepository Comments { get; } + IRepository CommentLikes { get; } + IRepository Follows { get; } + IRepository Notifications { get; } + + int Complete(); + Task CompleteAsync(); + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IUserRepository.cs b/AskFm/AskFm.DAL/Interfaces/IUserRepository.cs new file mode 100644 index 0000000..b35ed8e --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/IUserRepository.cs @@ -0,0 +1,9 @@ +using AskFm.DAL.Models; + +namespace AskFm.DAL.Interfaces; + +public interface IUserRepository : IRepository +{ + Task GetByUsernameAsync(string username); + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/CommentLikeRepository.cs b/AskFm/AskFm.DAL/Repositories/CommentLikeRepository.cs new file mode 100644 index 0000000..0dcfaeb --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/CommentLikeRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Repositories; + +public class CommentLikeRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/CommentRepository.cs b/AskFm/AskFm.DAL/Repositories/CommentRepository.cs new file mode 100644 index 0000000..9d14005 --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/CommentRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Repositories; + +public class CommentRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/FollowRepository.cs b/AskFm/AskFm.DAL/Repositories/FollowRepository.cs new file mode 100644 index 0000000..94eb318 --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/FollowRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Repositories; + +public class FollowRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs new file mode 100644 index 0000000..7fa79f1 --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Repositories; + +public class NotificationRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/Repository.cs b/AskFm/AskFm.DAL/Repositories/Repository.cs new file mode 100644 index 0000000..bafe712 --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/Repository.cs @@ -0,0 +1,114 @@ +using System.Linq.Expressions; +using AskFm.DAL.Interfaces; +using Microsoft.EntityFrameworkCore; +namespace AskFm.DAL.Repositories; + +public class Repository : IRepository where T : class +{ + protected readonly AppDbContext _context; + private readonly DbSet _dbSet; + + public Repository(AppDbContext context) + { + _context = context; + _dbSet = context.Set(); + } + + + + public IQueryable GetAll() => _dbSet.AsQueryable(); + public async Task> GetAllAsync() => await _dbSet.ToListAsync(); + + + + + public T? GetById(int id) => _dbSet.Find(id); + public async Task GetByIdAsync(int id) => await _dbSet.FindAsync(id); + + + + + public IQueryable FindAll(Expression> predicate, string[] includes = null) + { + IQueryable query = _dbSet; + if (includes != null) + { + foreach (var include in includes) + query = query.Include(include); + } + return query.Where(predicate); + } + + public async Task> FindAllAsync(Expression> predicate = null, string[] includes = null) + { + IQueryable query = _context.Set(); + + if (includes != null) + foreach (var include in includes) + query = query.Include(include); + + return await query.Where(predicate).ToListAsync(); + } + + + + + + public T? Find(Expression> predicate, string[] includes = null) + { + IQueryable query = _dbSet; + + if (includes != null) + { + foreach (var include in includes) + { + query = query.Include(include); + } + } + + return query.SingleOrDefault(predicate); + } + + public async Task FindAsync(Expression> predicate, string[] includes = null) + { + IQueryable query = _dbSet; + + if (includes != null) + { + foreach (var include in includes) + { + query = query.Include(include); + } + } + + return await query.FirstOrDefaultAsync(predicate); + } + + + + + public void Add(T entity) => _dbSet.Add(entity); + + public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity); + + + + + public void Update(T entity) => _dbSet.Update(entity); + + public Task UpdateAsync(T entity) + { + _dbSet.Update(entity); + return Task.CompletedTask; + } + + + public void Remove(T entity) => _dbSet.Remove(entity); + + public Task RemoveAsync(T entity) + { + _dbSet.Remove(entity); + return Task.CompletedTask; + } + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/SavedThreadRepository.cs b/AskFm/AskFm.DAL/Repositories/SavedThreadRepository.cs new file mode 100644 index 0000000..8168823 --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/SavedThreadRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Repositories; + +public class SavedThreadRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/ThreadLikeRepository.cs b/AskFm/AskFm.DAL/Repositories/ThreadLikeRepository.cs new file mode 100644 index 0000000..485f3ac --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/ThreadLikeRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Repositories; + +public class ThreadLikeRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/ThreadRepository.cs b/AskFm/AskFm.DAL/Repositories/ThreadRepository.cs new file mode 100644 index 0000000..c13a30b --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/ThreadRepository.cs @@ -0,0 +1,6 @@ +namespace AskFm.DAL.Repositories; + +public class ThreadRepository +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/UserRepository.cs b/AskFm/AskFm.DAL/Repositories/UserRepository.cs new file mode 100644 index 0000000..00d5af1 --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/UserRepository.cs @@ -0,0 +1,19 @@ +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using Microsoft.EntityFrameworkCore; + +namespace AskFm.DAL.Repositories; + +public class UserRepository : Repository, IUserRepository +{ + public UserRepository(AppDbContext context) : base(context) + { + } + + public async Task GetByUsernameAsync(string username) + { + return await _context.Users + .FirstOrDefaultAsync(u => u.Username == username); + } + +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/UnitOfWork.cs b/AskFm/AskFm.DAL/UnitOfWork.cs new file mode 100644 index 0000000..18030da --- /dev/null +++ b/AskFm/AskFm.DAL/UnitOfWork.cs @@ -0,0 +1,52 @@ +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using AskFm.DAL.Repositories; +using Thread = System.Threading.Thread; + +namespace AskFm.DAL; + +public class UnitOfWork : IUnitOfWork +{ + private readonly AppDbContext _context; + + public IRepository Users { get; private set; } + public IRepository Threads { get; private set; } + public IRepository SavedThreads { get; private set; } + public IRepository ThreadLikes { get; private set; } + public IRepository Comments { get; private set; } + public IRepository CommentLikes { get; private set; } + public IRepository Follows { get; private set; } + public IRepository Notifications { get; private set; } + + + public UnitOfWork(AppDbContext context) + { + _context = context; + + Users = new Repository(_context); + Threads = new Repository(_context); + SavedThreads = new Repository(_context); + ThreadLikes = new Repository(_context); + Comments = new Repository(_context); + CommentLikes = new Repository(_context); + Follows = new Repository(_context); + Notifications = new Repository(_context); + + } + + + public void Dispose() + { + _context.Dispose(); + } + + public int Complete() + { + return _context.SaveChanges(); + } + + public Task CompleteAsync() + { + return _context.SaveChangesAsync(); + } +} \ No newline at end of file From 4e151a536c298954f0ff3c4d0bdafe59aa559e1e Mon Sep 17 00:00:00 2001 From: Ziad Ashraf <155207557+ziad-ashraf7@users.noreply.github.com> Date: Sun, 10 Aug 2025 19:21:13 +0300 Subject: [PATCH 09/92] Update dotnet.yml --- .github/workflows/dotnet.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 6f0f72f..45f985a 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -2,9 +2,9 @@ name: .NET CI on: push: - branches: [ "main" ] + branches: [ "*" ] pull_request: - branches: [ "main" ] + branches: [ "*" ] jobs: build-and-test: From 4cd6aeb038515d9a1fee31e1e528ffb947a98013 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sun, 10 Aug 2025 19:26:13 +0300 Subject: [PATCH 10/92] FIX: some null exeptions has been handeled --- AskFm/AskFm.DAL/Repositories/Repository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AskFm/AskFm.DAL/Repositories/Repository.cs b/AskFm/AskFm.DAL/Repositories/Repository.cs index bafe712..ed826f6 100644 --- a/AskFm/AskFm.DAL/Repositories/Repository.cs +++ b/AskFm/AskFm.DAL/Repositories/Repository.cs @@ -39,9 +39,9 @@ public IQueryable FindAll(Expression> predicate, string[] inclu return query.Where(predicate); } - public async Task> FindAllAsync(Expression> predicate = null, string[] includes = null) + public async Task> FindAllAsync(Expression> predicate, string[] includes = null) { - IQueryable query = _context.Set(); + IQueryable query = _dbSet; if (includes != null) foreach (var include in includes) From a9468c12b49c6fda0cb53b9ad6fc8be3d388d20f Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 11 Aug 2025 21:07:32 +0300 Subject: [PATCH 11/92] FIX: UnitOfWork lazy init has been added --- AskFm/AskFm.API/Program.cs | 2 +- AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs | 4 +- AskFm/AskFm.DAL/Repositories/Repository.cs | 2 +- AskFm/AskFm.DAL/UnitOfWork.cs | 131 +++++++++++++++++---- 4 files changed, 112 insertions(+), 27 deletions(-) diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index abf9cf2..13232f4 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -29,7 +29,7 @@ public static void Main(string[] args) .UseLazyLoadingProxies() .UseSqlServer(ConnectionString)); - builder.Services.AddTransient(); + builder.Services.AddScoped(); var app = builder.Build(); diff --git a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs index 11871b2..4215d99 100644 --- a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs +++ b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs @@ -14,7 +14,7 @@ public interface IUnitOfWork : IDisposable IRepository Follows { get; } IRepository Notifications { get; } - int Complete(); - Task CompleteAsync(); + int Save(); + Task SaveAsync(); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/Repository.cs b/AskFm/AskFm.DAL/Repositories/Repository.cs index ed826f6..e928038 100644 --- a/AskFm/AskFm.DAL/Repositories/Repository.cs +++ b/AskFm/AskFm.DAL/Repositories/Repository.cs @@ -66,7 +66,7 @@ public async Task> FindAllAsync(Expression> predica } } - return query.SingleOrDefault(predicate); + return query.FirstOrDefault(predicate); } public async Task FindAsync(Expression> predicate, string[] includes = null) diff --git a/AskFm/AskFm.DAL/UnitOfWork.cs b/AskFm/AskFm.DAL/UnitOfWork.cs index 18030da..a38bf14 100644 --- a/AskFm/AskFm.DAL/UnitOfWork.cs +++ b/AskFm/AskFm.DAL/UnitOfWork.cs @@ -8,44 +8,129 @@ namespace AskFm.DAL; public class UnitOfWork : IUnitOfWork { private readonly AppDbContext _context; - - public IRepository Users { get; private set; } - public IRepository Threads { get; private set; } - public IRepository SavedThreads { get; private set; } - public IRepository ThreadLikes { get; private set; } - public IRepository Comments { get; private set; } - public IRepository CommentLikes { get; private set; } - public IRepository Follows { get; private set; } - public IRepository Notifications { get; private set; } + + private IRepository _users; + private IRepository _threads; + private IRepository _savedThreads; + private IRepository _threadLikes; + private IRepository _comments; + private IRepository _commentLikes; + private IRepository _follows; + private IRepository _notifications; + public UnitOfWork(AppDbContext context) { _context = context; - - Users = new Repository(_context); - Threads = new Repository(_context); - SavedThreads = new Repository(_context); - ThreadLikes = new Repository(_context); - Comments = new Repository(_context); - CommentLikes = new Repository(_context); - Follows = new Repository(_context); - Notifications = new Repository(_context); - } - - + public IRepository Users + { + get + { + if (_users == null) + { + _users = new Repository(_context); + } + return _users; + } + } + + public IRepository Threads { + get + { + if (_threads == null) + { + _threads = new Repository(_context); + } + return _threads; + } + } + + public IRepository SavedThreads + { + get + { + if (_savedThreads == null) + { + _savedThreads = new Repository(_context); + } + return _savedThreads; + } + } + + public IRepository ThreadLikes + { + get + { + if (_threadLikes == null) + { + _threadLikes = new Repository(_context); + } + return _threadLikes; + } + } + + public IRepository Comments + { + get + { + if (_comments == null) + { + _comments = new Repository(_context); + } + return _comments; + } + } + + public IRepository CommentLikes + { + get + { + if (_commentLikes == null) + { + _commentLikes = new Repository(_context); + } + return _commentLikes; + } + } + + public IRepository Follows + { + get + { + if (_follows == null) + { + _follows = new Repository(_context); + } + return _follows; + } + } + + public IRepository Notifications + { + get + { + if (_notifications == null) + { + _notifications = new Repository(_context); + } + return _notifications; + } + } + + public void Dispose() { _context.Dispose(); } - public int Complete() + public int Save() { return _context.SaveChanges(); } - public Task CompleteAsync() + public Task SaveAsync() { return _context.SaveChangesAsync(); } From 2982d6d665bde566d47faa141e21a9c1bb89df26 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Tue, 12 Aug 2025 16:36:55 +0300 Subject: [PATCH 12/92] Added: added Services , DTO folders --- AskFm/AskFm.BLL/AskFm.BLL.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index ba85168..974046b 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -11,4 +11,9 @@ + + + + + From eaea1427366e4dc1fcea0d5d7da00c5ee3002048 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Tue, 12 Aug 2025 16:53:50 +0300 Subject: [PATCH 13/92] added Class to the DTO , Services folders --- AskFm/AskFm.BLL/DTO/testDTO.cs | 6 ++++++ AskFm/AskFm.BLL/Services/testServices.cs | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 AskFm/AskFm.BLL/DTO/testDTO.cs create mode 100644 AskFm/AskFm.BLL/Services/testServices.cs diff --git a/AskFm/AskFm.BLL/DTO/testDTO.cs b/AskFm/AskFm.BLL/DTO/testDTO.cs new file mode 100644 index 0000000..7752c44 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/testDTO.cs @@ -0,0 +1,6 @@ +namespace AskFm.BLL.DTO; + +public class testDTO +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/testServices.cs b/AskFm/AskFm.BLL/Services/testServices.cs new file mode 100644 index 0000000..b618562 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/testServices.cs @@ -0,0 +1,6 @@ +namespace AskFm.BLL.Services; + +public class testServices +{ + +} \ No newline at end of file From 0982c34bcd8548e2459cfa291d1c62be24c43564 Mon Sep 17 00:00:00 2001 From: hadeer Date: Tue, 12 Aug 2025 18:05:30 +0300 Subject: [PATCH 14/92] Feat: Adding More infromation to track in ITrackable Interface --- AskFm/AskFm.DAL/AppDbContext.cs | 7 + ...20250812150332_Track_more_info.Designer.cs | 757 ++++++++++++++++++ .../20250812150332_Track_more_info.cs | 264 ++++++ .../Migrations/AppDbContextModelSnapshot.cs | 47 +- AskFm/AskFm.DAL/Models/ApplicationUser.cs | 5 +- AskFm/AskFm.DAL/Models/Comment.cs | 3 +- AskFm/AskFm.DAL/Models/CommentLike.cs | 3 +- AskFm/AskFm.DAL/Models/Follow.cs | 5 +- AskFm/AskFm.DAL/Models/ITrackable.cs | 2 + AskFm/AskFm.DAL/Models/Notification.cs | 4 +- AskFm/AskFm.DAL/Models/SavedThreads.cs | 2 + AskFm/AskFm.DAL/Models/Thread.cs | 3 +- AskFm/AskFm.DAL/Models/ThreadLike.cs | 3 +- .../CommentConfigration.cs | 4 - .../CommentLikeConfigration.cs | 6 +- .../NotificationConfigration.cs | 5 +- .../ModelsConfigrations/ThreadConfigration.cs | 3 - .../ThreadLikeConfigration.cs | 6 +- 18 files changed, 1091 insertions(+), 38 deletions(-) create mode 100644 AskFm/AskFm.DAL/Migrations/20250812150332_Track_more_info.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250812150332_Track_more_info.cs diff --git a/AskFm/AskFm.DAL/AppDbContext.cs b/AskFm/AskFm.DAL/AppDbContext.cs index 557e5f4..f415e4c 100644 --- a/AskFm/AskFm.DAL/AppDbContext.cs +++ b/AskFm/AskFm.DAL/AppDbContext.cs @@ -30,9 +30,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) foreach (var entityType in modelBuilder.Model.GetEntityTypes() .Where(t => typeof(ITrackable).IsAssignableFrom(t.ClrType))) { + modelBuilder.Entity(entityType.ClrType).Property("DeletedAt") .HasColumnType("DATETIME"); + modelBuilder.Entity(entityType.ClrType).Property("UpdatedAt") + .HasColumnType("DATETIME"); + + modelBuilder.Entity(entityType.ClrType).Property("CreatedAt") + .HasColumnType("DATETIME"); + modelBuilder.Entity(entityType.ClrType).Property("IsDeleted") .HasColumnType("BIT") .HasDefaultValue(false); diff --git a/AskFm/AskFm.DAL/Migrations/20250812150332_Track_more_info.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250812150332_Track_more_info.Designer.cs new file mode 100644 index 0000000..3e74804 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250812150332_Track_more_info.Designer.cs @@ -0,0 +1,757 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250812150332_Track_more_info")] + partial class Track_more_info + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("isRead") + .HasColumnType("bit"); + + b.Property("jsonContent") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250812150332_Track_more_info.cs b/AskFm/AskFm.DAL/Migrations/20250812150332_Track_more_info.cs new file mode 100644 index 0000000..28ec5aa --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250812150332_Track_more_info.cs @@ -0,0 +1,264 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class Track_more_info : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AlterColumn( + name: "IsDeleted", + table: "AspNetUsers", + type: "BIT", + nullable: false, + defaultValue: false, + oldClrType: typeof(bool), + oldType: "bit"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "ThreadLikes"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "Thread"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "SavedThreads"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "SavedThreads"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "Notifications"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "Follows"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "Comments"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "CommentLikes"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "AspNetUsers"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ThreadLikes", + type: "datetime", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Thread", + type: "datetime", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Notifications", + type: "datetime", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Follows", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Comments", + type: "datetime", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "CommentLikes", + type: "datetime", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "IsDeleted", + table: "AspNetUsers", + type: "bit", + nullable: false, + oldClrType: typeof(bool), + oldType: "BIT", + oldDefaultValue: false); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index 34f3467..0d3ad2e 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -47,10 +47,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("CreatedAt") - .HasColumnType("datetime2"); + .HasColumnType("DATETIME"); b.Property("DeletedAt") - .HasColumnType("datetime2"); + .HasColumnType("DATETIME"); b.Property("Email") .IsRequired() @@ -67,7 +67,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("IsDeleted") - .HasColumnType("bit"); + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); b.Property("LastSeen") .HasColumnType("datetime2"); @@ -107,6 +109,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("TwoFactorEnabled") .HasColumnType("bit"); + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + b.Property("UserName") .IsRequired() .HasMaxLength(256) @@ -142,7 +147,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(1000)"); b.Property("CreatedAt") - .HasColumnType("datetime"); + .HasColumnType("DATETIME"); b.Property("DeletedAt") .HasColumnType("DATETIME"); @@ -161,6 +166,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ThreadId") .HasColumnType("int"); + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + b.Property("UserId") .HasColumnType("int"); @@ -184,7 +192,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("datetime"); + .HasColumnType("DATETIME"); b.Property("DeletedAt") .HasColumnType("DATETIME"); @@ -194,6 +202,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("BIT") .HasDefaultValue(false); + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + b.HasKey("UserId", "CommentId"); b.HasIndex("CommentId"); @@ -210,7 +221,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("datetime2"); + .HasColumnType("DATETIME"); b.Property("DeletedAt") .HasColumnType("DATETIME"); @@ -223,6 +234,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("BIT") .HasDefaultValue(false); + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + b.HasKey("FollowerId", "FollowedId"); b.HasIndex("FollowedId"); @@ -239,7 +253,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("datetime"); + .HasColumnType("DATETIME"); b.Property("DeletedAt") .HasColumnType("DATETIME"); @@ -252,6 +266,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ResourceId") .HasColumnType("int"); + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + b.Property("UserId") .HasColumnType("int"); @@ -277,6 +294,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("UserId") .HasColumnType("int"); + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + b.Property("DeletedAt") .HasColumnType("DATETIME"); @@ -285,6 +305,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("BIT") .HasDefaultValue(false); + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + b.HasKey("SavedThreadId", "UserId"); b.HasIndex("UserId"); @@ -312,7 +335,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("datetime"); + .HasColumnType("DATETIME"); b.Property("DeletedAt") .HasColumnType("DATETIME"); @@ -331,6 +354,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + b.Property("isAnonymous") .HasColumnType("bit"); @@ -352,7 +378,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("datetime"); + .HasColumnType("DATETIME"); b.Property("DeletedAt") .HasColumnType("DATETIME"); @@ -362,6 +388,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("BIT") .HasDefaultValue(false); + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + b.HasKey("ThreadId", "UserId"); b.HasIndex("UserId"); diff --git a/AskFm/AskFm.DAL/Models/ApplicationUser.cs b/AskFm/AskFm.DAL/Models/ApplicationUser.cs index 757b7ff..f45c39f 100644 --- a/AskFm/AskFm.DAL/Models/ApplicationUser.cs +++ b/AskFm/AskFm.DAL/Models/ApplicationUser.cs @@ -3,7 +3,7 @@ namespace AskFm.DAL.Models; -public class ApplicationUser : IdentityUser +public class ApplicationUser : IdentityUser, ITrackable { public string Name { get; set; } public string Email { get; set; } @@ -13,7 +13,6 @@ public class ApplicationUser : IdentityUser public int FollowersCount { get; set; } public int FollowingCount { get; set; } - public DateTime CreatedAt { get; set; } public DateTime LastSeen { get; set; } public virtual ICollection? AskedThreads { get; set; } @@ -28,4 +27,6 @@ public class ApplicationUser : IdentityUser public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Comment.cs b/AskFm/AskFm.DAL/Models/Comment.cs index 3a53aeb..b936b24 100644 --- a/AskFm/AskFm.DAL/Models/Comment.cs +++ b/AskFm/AskFm.DAL/Models/Comment.cs @@ -7,7 +7,6 @@ public class Comment : ITrackable { public int Id { get; set; } public string Content { get; set; } - public DateTime CreatedAt { get; set; } public int LikeCount { get; set; } public int? UserId { get; set; } @@ -23,4 +22,6 @@ public class Comment : ITrackable public virtual ICollection? CommentLikes { get; set; } public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/CommentLike.cs b/AskFm/AskFm.DAL/Models/CommentLike.cs index cd49b27..b75e030 100644 --- a/AskFm/AskFm.DAL/Models/CommentLike.cs +++ b/AskFm/AskFm.DAL/Models/CommentLike.cs @@ -8,7 +8,8 @@ public class CommentLike : ITrackable public virtual Comment? Comment { get; set; } public int UserId { get; set; } public virtual ApplicationUser? User { get; set; } - public DateTime CreatedAt { get; set; } public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Follow.cs b/AskFm/AskFm.DAL/Models/Follow.cs index 1bccfe6..1c41965 100644 --- a/AskFm/AskFm.DAL/Models/Follow.cs +++ b/AskFm/AskFm.DAL/Models/Follow.cs @@ -9,10 +9,13 @@ public class Follow : ITrackable public int FollowedId { get; set; } public virtual ApplicationUser? Followed { get; set; } - public DateTime CreatedAt { get; set; } // is this follow available public bool IsActive { get; set; } = true; + + public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/ITrackable.cs b/AskFm/AskFm.DAL/Models/ITrackable.cs index 8f1f498..ec0dd36 100644 --- a/AskFm/AskFm.DAL/Models/ITrackable.cs +++ b/AskFm/AskFm.DAL/Models/ITrackable.cs @@ -6,4 +6,6 @@ public interface ITrackable { bool IsDeleted { get; set; } DateTime DeletedAt { get; set; } + DateTime UpdatedAt { get; set; } + DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index c4eda71..0ea01ef 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -12,7 +12,9 @@ public class Notification : ITrackable public int ResourceId { get; set; } public string jsonContent { get; set; } - public DateTime CreatedAt { get; set; } + public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/SavedThreads.cs b/AskFm/AskFm.DAL/Models/SavedThreads.cs index 95daf5a..d856d68 100644 --- a/AskFm/AskFm.DAL/Models/SavedThreads.cs +++ b/AskFm/AskFm.DAL/Models/SavedThreads.cs @@ -11,4 +11,6 @@ public class SavedThreads : ITrackable public virtual ApplicationUser? User { get; set; } public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Thread.cs b/AskFm/AskFm.DAL/Models/Thread.cs index 86c29bd..949b163 100644 --- a/AskFm/AskFm.DAL/Models/Thread.cs +++ b/AskFm/AskFm.DAL/Models/Thread.cs @@ -10,7 +10,6 @@ public class Thread : ITrackable public ThreadStatus Status { get; set; } public bool isAnonymous { get; set; } - public DateTime CreatedAt { get; set; } public int? AskerId { get; set; } public virtual ApplicationUser? Asker { get; set; } @@ -22,4 +21,6 @@ public class Thread : ITrackable public virtual ICollection? SavedThreads { get; set; } public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/ThreadLike.cs b/AskFm/AskFm.DAL/Models/ThreadLike.cs index f185c71..a1ad625 100644 --- a/AskFm/AskFm.DAL/Models/ThreadLike.cs +++ b/AskFm/AskFm.DAL/Models/ThreadLike.cs @@ -10,7 +10,8 @@ public class ThreadLike : ITrackable public int UserId { get; set; } public virtual ApplicationUser? User { get; set; } - public DateTime CreatedAt { get; set; } public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime CreatedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs index 97e7bd7..a02f7ef 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs @@ -14,10 +14,6 @@ public void Configure(EntityTypeBuilder builder) .IsRequired() .HasMaxLength(1000); - builder.Property(c => c.CreatedAt) - .HasColumnType("datetime") - .IsRequired(); - builder.HasOne(c => c.Thread) .WithMany(t => t.Comments) diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/CommentLikeConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/CommentLikeConfigration.cs index 0de2c89..b8b8e9d 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/CommentLikeConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/CommentLikeConfigration.cs @@ -9,11 +9,7 @@ public class CommentLikeConfigration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.HasKey(cl => new { cl.UserId, cl.CommentId }); - - builder.Property(cl => cl.CreatedAt) - .HasColumnType("datetime") - .IsRequired(); - + builder.HasOne(cl => cl.User) .WithMany(u => u.CommentLikes) .HasForeignKey(cl => cl.UserId) diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs index 1894dae..afeda9b 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs @@ -15,10 +15,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(n => n.isRead) .IsRequired(); - - builder.Property(n => n.CreatedAt) - .HasColumnType("datetime") - .IsRequired(); + builder.HasOne(n => n.User) .WithMany(u => u.Notifications) diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/ThreadConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/ThreadConfigration.cs index 2297076..10cf68a 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/ThreadConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/ThreadConfigration.cs @@ -19,9 +19,6 @@ public void Configure(EntityTypeBuilder builder) builder.Property(t => t.AnswerContent) .HasMaxLength(4000); - builder.Property(t => t.CreatedAt) - .HasColumnType("datetime") - .IsRequired(); builder.Property(t => t.isAnonymous) .IsRequired(); diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/ThreadLikeConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/ThreadLikeConfigration.cs index 19646be..1c6e96c 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/ThreadLikeConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/ThreadLikeConfigration.cs @@ -9,11 +9,7 @@ public class ThreadLikeConfigration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.HasKey(tl => new { tl.ThreadId, tl.UserId }); - - builder.Property(ql => ql.CreatedAt) - .HasColumnType("datetime") - .IsRequired(); - + builder.HasOne(tl => tl.User) .WithMany(u => u.ThreadLikes) .HasForeignKey(tl => tl.UserId) From 5d7a91110055369c0e4d1d7a276cd6261282f7b9 Mon Sep 17 00:00:00 2001 From: hadeer Date: Tue, 12 Aug 2025 18:22:08 +0300 Subject: [PATCH 15/92] Feat: Override Savechanges and SaveChangesAsync to write trackable data every time updating database --- AskFm/AskFm.DAL/AppDbContext.cs | 37 ++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/AskFm/AskFm.DAL/AppDbContext.cs b/AskFm/AskFm.DAL/AppDbContext.cs index f415e4c..4f35d3d 100644 --- a/AskFm/AskFm.DAL/AppDbContext.cs +++ b/AskFm/AskFm.DAL/AppDbContext.cs @@ -30,7 +30,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) foreach (var entityType in modelBuilder.Model.GetEntityTypes() .Where(t => typeof(ITrackable).IsAssignableFrom(t.ClrType))) { - + modelBuilder.Entity(entityType.ClrType).Property("DeletedAt") .HasColumnType("DATETIME"); @@ -46,4 +46,39 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) + { + _fillTrackableInfromation(); + return base.SaveChangesAsync(cancellationToken); + + } + public override int SaveChanges() + { + _fillTrackableInfromation(); + return base.SaveChanges(); + } + private void _fillTrackableInfromation() + { + foreach (var entry in ChangeTracker.Entries()) + { + switch (entry.State) + { + case EntityState.Added: + entry.Entity.CreatedAt = DateTime.UtcNow; + entry.Entity.DeletedAt = DateTime.UtcNow;; + entry.Entity.UpdatedAt = DateTime.UtcNow; + entry.Entity.IsDeleted = false; + break; + case EntityState.Modified: + entry.Entity.UpdatedAt = DateTime.UtcNow; + break; + case EntityState.Deleted: + entry.Entity.IsDeleted = true; + entry.Entity.DeletedAt = DateTime.UtcNow; + entry.State = EntityState.Modified; + break; + } + } + } } \ No newline at end of file From 0d498eea42a2a579672e28bef66161ca18e73175 Mon Sep 17 00:00:00 2001 From: hadeer Date: Tue, 12 Aug 2025 18:45:46 +0300 Subject: [PATCH 16/92] Fix: Solve conflict betwenn User & ApplicationUser --- AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs | 2 +- AskFm/AskFm.DAL/Interfaces/IUserRepository.cs | 4 ++-- AskFm/AskFm.DAL/Repositories/UserRepository.cs | 6 +++--- AskFm/AskFm.DAL/UnitOfWork.cs | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs index 4215d99..108f184 100644 --- a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs +++ b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs @@ -5,7 +5,7 @@ namespace AskFm.DAL.Interfaces; public interface IUnitOfWork : IDisposable { - IRepository Users { get; } + IRepository Users { get; } IRepository Threads { get; } IRepository SavedThreads { get; } IRepository ThreadLikes { get; } diff --git a/AskFm/AskFm.DAL/Interfaces/IUserRepository.cs b/AskFm/AskFm.DAL/Interfaces/IUserRepository.cs index b35ed8e..20cf2e5 100644 --- a/AskFm/AskFm.DAL/Interfaces/IUserRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/IUserRepository.cs @@ -2,8 +2,8 @@ namespace AskFm.DAL.Interfaces; -public interface IUserRepository : IRepository +public interface IApplicationUserRepository : IRepository { - Task GetByUsernameAsync(string username); + Task GetByUsernameAsync(string username); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/UserRepository.cs b/AskFm/AskFm.DAL/Repositories/UserRepository.cs index 00d5af1..dcf55f3 100644 --- a/AskFm/AskFm.DAL/Repositories/UserRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/UserRepository.cs @@ -4,16 +4,16 @@ namespace AskFm.DAL.Repositories; -public class UserRepository : Repository, IUserRepository +public class UserRepository : Repository, IApplicationUserRepository { public UserRepository(AppDbContext context) : base(context) { } - public async Task GetByUsernameAsync(string username) + public async Task GetByUsernameAsync(string username) { return await _context.Users - .FirstOrDefaultAsync(u => u.Username == username); + .FirstOrDefaultAsync(u => u.UserName == username); } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/UnitOfWork.cs b/AskFm/AskFm.DAL/UnitOfWork.cs index a38bf14..573f3ae 100644 --- a/AskFm/AskFm.DAL/UnitOfWork.cs +++ b/AskFm/AskFm.DAL/UnitOfWork.cs @@ -9,7 +9,7 @@ public class UnitOfWork : IUnitOfWork { private readonly AppDbContext _context; - private IRepository _users; + private IRepository _users; private IRepository _threads; private IRepository _savedThreads; private IRepository _threadLikes; @@ -24,13 +24,13 @@ public UnitOfWork(AppDbContext context) { _context = context; } - public IRepository Users + public IRepository Users { get { if (_users == null) { - _users = new Repository(_context); + _users = new Repository(_context); } return _users; } From 65db53c702c4b3f1f5526af07e167fe8b2e74b74 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sat, 16 Aug 2025 11:09:01 +0300 Subject: [PATCH 17/92] Modified the Testing framework to xUnit --- AskFm/AskFm.sln | 12 ++++++------ AskFm/Tests/Tests.csproj | 8 +++----- AskFm/Tests/UnitTest1.cs | 10 ++-------- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/AskFm/AskFm.sln b/AskFm/AskFm.sln index 76829e6..dad7918 100644 --- a/AskFm/AskFm.sln +++ b/AskFm/AskFm.sln @@ -9,10 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AskFm.BLL", "AskFm.BLL\AskF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AskFm.DAL", "AskFm.DAL\AskFm.DAL.csproj", "{DABC5408-AC02-42E5-B4D7-9CE6FA449BA6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{CE580260-EB9E-4AFE-8ED5-34EF36BD5006}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared.csproj", "{4D728DA7-0DB2-4588-8DB5-453F8C06BC63}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{2830F095-ECA5-4A41-BB9A-629752F0AF77}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,14 +31,14 @@ Global {DABC5408-AC02-42E5-B4D7-9CE6FA449BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU {DABC5408-AC02-42E5-B4D7-9CE6FA449BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU {DABC5408-AC02-42E5-B4D7-9CE6FA449BA6}.Release|Any CPU.Build.0 = Release|Any CPU - {CE580260-EB9E-4AFE-8ED5-34EF36BD5006}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE580260-EB9E-4AFE-8ED5-34EF36BD5006}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CE580260-EB9E-4AFE-8ED5-34EF36BD5006}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CE580260-EB9E-4AFE-8ED5-34EF36BD5006}.Release|Any CPU.Build.0 = Release|Any CPU {4D728DA7-0DB2-4588-8DB5-453F8C06BC63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4D728DA7-0DB2-4588-8DB5-453F8C06BC63}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D728DA7-0DB2-4588-8DB5-453F8C06BC63}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D728DA7-0DB2-4588-8DB5-453F8C06BC63}.Release|Any CPU.Build.0 = Release|Any CPU + {2830F095-ECA5-4A41-BB9A-629752F0AF77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2830F095-ECA5-4A41-BB9A-629752F0AF77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2830F095-ECA5-4A41-BB9A-629752F0AF77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2830F095-ECA5-4A41-BB9A-629752F0AF77}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AskFm/Tests/Tests.csproj b/AskFm/Tests/Tests.csproj index d3517ca..95d873d 100644 --- a/AskFm/Tests/Tests.csproj +++ b/AskFm/Tests/Tests.csproj @@ -2,7 +2,6 @@ net9.0 - latest enable enable false @@ -11,13 +10,12 @@ - - - + + - + diff --git a/AskFm/Tests/UnitTest1.cs b/AskFm/Tests/UnitTest1.cs index 5e933a4..4e05716 100644 --- a/AskFm/Tests/UnitTest1.cs +++ b/AskFm/Tests/UnitTest1.cs @@ -1,15 +1,9 @@ namespace Tests; -public class Tests +public class UnitTest1 { - [SetUp] - public void Setup() - { - } - - [Test] + [Fact] public void Test1() { - Assert.Pass(); } } \ No newline at end of file From 3f17e7fe0f7da56b0cb17f0547ce48a5477094c7 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Sun, 17 Aug 2025 12:37:46 +0300 Subject: [PATCH 18/92] architecture of user APIs & Services --- AskFm/AskFm.API/AskFm.API.csproj | 1 + AskFm/AskFm.API/Controllers/UserController.cs | 20 +++++++++++ AskFm/AskFm.API/Program.cs | 15 +++++++-- AskFm/AskFm.BLL/AskFm.BLL.csproj | 5 --- AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs | 6 ++++ AskFm/AskFm.BLL/DTO/UserDTOs/ReadUserDTO.cs | 11 +++++++ .../AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs | 11 +++++++ AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs | 10 ++++++ AskFm/AskFm.BLL/DTO/testDTO.cs | 6 ---- AskFm/AskFm.BLL/Services/ServiceResult.cs | 33 +++++++++++++++++++ .../UserIdentityService/IAuthService.cs | 12 +++++++ .../UserIdentityService/IUserService.cs | 13 ++++++++ 12 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 AskFm/AskFm.API/Controllers/UserController.cs create mode 100644 AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs create mode 100644 AskFm/AskFm.BLL/DTO/UserDTOs/ReadUserDTO.cs create mode 100644 AskFm/AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs create mode 100644 AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs delete mode 100644 AskFm/AskFm.BLL/DTO/testDTO.cs create mode 100644 AskFm/AskFm.BLL/Services/ServiceResult.cs create mode 100644 AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs create mode 100644 AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index f10f941..c71e10c 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -18,6 +18,7 @@ + diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs new file mode 100644 index 0000000..8335b50 --- /dev/null +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; + +namespace AskFm.API.Controllers; + +[ApiController] +[Route("[controller]")] +public class UserController : ControllerBase +{ + /* + Register + login + logout + EditUser + DeleteUser + FollowUser + unfollowUser + Helper Function: getCurrentUserId + */ + +} \ No newline at end of file diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index ce07055..1da703b 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -6,6 +6,8 @@ using AskFm.DAL.Repositories; using DotNetEnv; using Microsoft.EntityFrameworkCore.Proxies; +using Microsoft.OpenApi.Models; + namespace AskFm.API; @@ -39,12 +41,19 @@ public static void Main(string[] args) builder.Services.AddScoped(); - - + builder.Services.AddControllers(); + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + var app = builder.Build(); // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) app.MapOpenApi(); + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + app.MapOpenApi(); + } app.UseHttpsRedirection(); diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index 974046b..ba85168 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -11,9 +11,4 @@ - - - - - diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs new file mode 100644 index 0000000..01913c2 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs @@ -0,0 +1,6 @@ +namespace AskFm.BLL.DTO.UserDTOs; + +public class LoginDTO +{ + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/ReadUserDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/ReadUserDTO.cs new file mode 100644 index 0000000..dfaf4ca --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/ReadUserDTO.cs @@ -0,0 +1,11 @@ +namespace AskFm.BLL.DTO.UserDTOs; + +public class ReadUserDTO +{ + public string Name { get; set; } + public string Email { get; set; } + public DateTime LastSeen { get; set; } + public string Bio { get; set; } + public string AvatarPath { get; set; } + public int followerCount { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs new file mode 100644 index 0000000..23a5bcf --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs @@ -0,0 +1,11 @@ +namespace AskFm.BLL.DTO.UserDTOs; + +public class RegisterUserDTO +{ + public string Name { get; set; } + public string Email { get; set; } + public string Bio { get; set; } + public string AvatarPath { get; set; } + public string Passwrod { get; set; } + public DateTime LastSeen { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs new file mode 100644 index 0000000..3f13b89 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs @@ -0,0 +1,10 @@ +namespace AskFm.BLL.DTO.UserDTOs; + +public class UpdateUserDTO +{ + public string Name { get; set; } + public string Email { get; set; } + public string Bio { get; set; } + public string AvatarPath { get; set; } + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/testDTO.cs b/AskFm/AskFm.BLL/DTO/testDTO.cs deleted file mode 100644 index 7752c44..0000000 --- a/AskFm/AskFm.BLL/DTO/testDTO.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace AskFm.BLL.DTO; - -public class testDTO -{ - -} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ServiceResult.cs b/AskFm/AskFm.BLL/Services/ServiceResult.cs new file mode 100644 index 0000000..b8af2fa --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ServiceResult.cs @@ -0,0 +1,33 @@ +namespace AskFm.BLL.Services; + +public class ServiceResult +{ + public bool success { get; set; } + public List? Errors { get; set; } + public T? Data { get; set; } + + public static ServiceResult Success(T data) + { + return new ServiceResult + { + success = true, + Data = data + }; + } + public static ServiceResult Success() + { + return new ServiceResult + { + success = true, + }; + } + + public static ServiceResult Failure(List errors) + { + return new ServiceResult + { + success = false, + Errors = errors + }; + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs new file mode 100644 index 0000000..855b766 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs @@ -0,0 +1,12 @@ +using AskFm.BLL.DTO.UserDTOs; + +namespace AskFm.BLL.Services.UserIdentityService; + +public interface IAuthService +{ + public Task> LoginAsync(LoginDTO request); + public Task> RegisterAsync(RegisterUserDTO request); + // Task> RefreshTokenAsync(string refreshToken); + public Task> RevokeRefreshTokenAsync(string refreshToken); + public Task> RevokeAllRefreshTokensAsync(int userId); +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs new file mode 100644 index 0000000..790adf4 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -0,0 +1,13 @@ +using AskFm.BLL.DTO.UserDTOs; + +namespace AskFm.BLL.Services.UserIdentityService; + +public interface IUserService +{ + Task> UpdateUserAsync(int userId, UpdateUserDTO updatedUser); + Task> DeleteUserAsync(int userId); + Task> FollowUserAsync(int followerId, int targetUserId); + Task> UnfollowUserAsync(int followerId, int targetUserId); + Task UpdateLastSeenAsync(int userId, DateTime lastSeen); + Task GetUserByIdAsync(int userId); +} From 888cfb307397db0f56176cfffb350bed63c4137e Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Mon, 18 Aug 2025 02:25:08 +0300 Subject: [PATCH 19/92] Add SignalR Package --- AskFm/AskFm.API/AskFm.API.csproj | 51 +++++++++---------- .../Controllers/NotificationController.cs | 0 2 files changed, 24 insertions(+), 27 deletions(-) create mode 100644 AskFm/AskFm.API/Controllers/NotificationController.cs diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index f10f941..6957d66 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -1,28 +1,25 @@ - - - net9.0 - enable - enable - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - + + net9.0 + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + \ No newline at end of file diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs new file mode 100644 index 0000000..e69de29 From ad90d92f6b9597c5af387638130055fe2ec0bdc1 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Mon, 18 Aug 2025 02:57:33 +0300 Subject: [PATCH 20/92] Add Notification Hub --- AskFm/AskFm.API/Program.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index ce07055..46be5c6 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -6,6 +6,7 @@ using AskFm.DAL.Repositories; using DotNetEnv; using Microsoft.EntityFrameworkCore.Proxies; +using AskFm.BLL.Hub; namespace AskFm.API; @@ -41,6 +42,7 @@ public static void Main(string[] args) builder.Services.AddScoped(); + builder.Services.AddSignalR(); var app = builder.Build(); // Configure the HTTP request pipeline. @@ -53,6 +55,8 @@ public static void Main(string[] args) app.MapControllers(); + app.MapHub("/notificationHub"); + app.Run(); } } \ No newline at end of file From 47f9e2bcc01f2ed435a8a1f1f7b9bd3b8aab9f3f Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Mon, 18 Aug 2025 17:26:16 +0300 Subject: [PATCH 21/92] Implement NotificationDto --- AskFm/AskFm.BLL/AskFm.BLL.csproj | 37 +++++----- AskFm/AskFm.BLL/DTO/NotificationDto.cs | 13 ++++ AskFm/AskFm.BLL/Hub/NotificationHub.cs | 12 ++++ .../AskFm.BLL/Services/NotificationService.cs | 0 AskFm/AskFm.DAL/AskFm.DAL.csproj | 68 +++++++++---------- 5 files changed, 75 insertions(+), 55 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/NotificationDto.cs create mode 100644 AskFm/AskFm.BLL/Hub/NotificationHub.cs create mode 100644 AskFm/AskFm.BLL/Services/NotificationService.cs diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index 974046b..2fcc00b 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -1,19 +1,18 @@ - - - - net9.0 - enable - enable - - - - - - - - - - - - - + + + net9.0 + enable + enable + + + + + + + + + + + + + \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/NotificationDto.cs b/AskFm/AskFm.BLL/DTO/NotificationDto.cs new file mode 100644 index 0000000..2256cff --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/NotificationDto.cs @@ -0,0 +1,13 @@ +public class NotificationDto +{ + public int Id { get; set; } + public string Type { get; set; } + + public string Message { get; set; } + + public bool IsRead { get; set; } + public DateTime CreatedAt { get; set; } + + public int ResourceId { get; set; } + public int UserId { get; set; } +} diff --git a/AskFm/AskFm.BLL/Hub/NotificationHub.cs b/AskFm/AskFm.BLL/Hub/NotificationHub.cs new file mode 100644 index 0000000..4b98ecb --- /dev/null +++ b/AskFm/AskFm.BLL/Hub/NotificationHub.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace AskFm.BLL.Hub +{ + public class NotificationHub : Microsoft.AspNetCore.SignalR.Hub + { + + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs new file mode 100644 index 0000000..e69de29 diff --git a/AskFm/AskFm.DAL/AskFm.DAL.csproj b/AskFm/AskFm.DAL/AskFm.DAL.csproj index c77d955..e33b6a5 100644 --- a/AskFm/AskFm.DAL/AskFm.DAL.csproj +++ b/AskFm/AskFm.DAL/AskFm.DAL.csproj @@ -1,36 +1,32 @@ - - - - net9.0 - enable - enable - AskFm.DAL - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - ..\..\..\..\.nuget\packages\microsoft.entityframeworkcore.relational\9.0.7\lib\net8.0\Microsoft.EntityFrameworkCore.Relational.dll - - - - - - - - + + + net9.0 + enable + enable + AskFm.DAL + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + ..\..\..\..\.nuget\packages\microsoft.entityframeworkcore.relational\9.0.7\lib\net8.0\Microsoft.EntityFrameworkCore.Relational.dll + + + + + + \ No newline at end of file From fe6259ae03f63faa4282250124884c46c07908f2 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Mon, 18 Aug 2025 21:12:30 +0300 Subject: [PATCH 22/92] Implement Notification Repository --- .../Interfaces/INotificationRepository.cs | 9 ++- AskFm/AskFm.DAL/Models/Notification.cs | 3 +- .../Repositories/NotificationRepository.cs | 65 ++++++++++++++++++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs index d2883fd..e0d577c 100644 --- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -1,6 +1,13 @@ +using AskFm.DAL.Enums; +using AskFm.DAL.Models; + namespace AskFm.DAL.Interfaces; public interface INotificationRepository { - + Task> GetAllNotifications(int userId, int pageNumber, int pageSize); + Task GetNotificationById(int notificationId); + Task UpdateNotification(Notification notification); + Task AddNotification(Notification notification); + Task> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index 0ea01ef..09469fb 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -1,10 +1,11 @@ using System.Runtime.InteropServices.JavaScript; +using AskFm.DAL.Enums; namespace AskFm.DAL.Models; public class Notification : ITrackable { - public GCNotificationStatus Type; + public NotificationStatus Type; public int Id { get; set; } public int UserId { get; set; } public virtual ApplicationUser? User { get; set; } diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs index 7fa79f1..37a96f7 100644 --- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -1,6 +1,69 @@ +using AskFm.DAL.Enums; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using Microsoft.EntityFrameworkCore; + namespace AskFm.DAL.Repositories; -public class NotificationRepository +public class NotificationRepository : INotificationRepository { + private readonly AppDbContext _context; + public NotificationRepository(AppDbContext context) + { + _context = context; + } + public async Task> GetAllNotifications(int userId, int pageNumber, int pageSize) + { + var totalCount = await _context.Notifications.CountAsync(n => n.UserId == userId); + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var notifications = await _context.Notifications + .Where(n => n.UserId == userId) + .OrderByDescending(n => n.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + return notifications; + } + + public async Task GetNotificationById(int notificationId) + { + var notification = await _context.Notifications.FindAsync(notificationId); + if (notification == null) + throw new InvalidOperationException($"Notification with ID {notificationId} not found."); + return notification; + } + + public async Task UpdateNotification(Notification notification) + { + if (notification == null) + throw new ArgumentNullException(nameof(notification)); + + _context.Notifications.Update(notification); + await _context.SaveChangesAsync(); + } + + public async Task AddNotification(Notification notification) + { + if (notification == null) + throw new ArgumentNullException(nameof(notification)); + + await _context.Notifications.AddAsync(notification); + await _context.SaveChangesAsync(); + } + public Task> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) + { + var query = _context.Notifications.AsQueryable(); + + if (userId > 0) + { + query = query.Where(n => n.UserId == userId); + } + + query = query.Where(n => n.Type == status); + + return Task.FromResult(query.OrderByDescending(n => n.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize)); + } } \ No newline at end of file From 6c8330e41b01d40a55b0133b1b1a3f9344f76cf7 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Tue, 19 Aug 2025 02:32:50 +0300 Subject: [PATCH 23/92] fix: update notification model and repository for API compliance --- AskFm/AskFm.BLL/AskFm.BLL.csproj | 1 - AskFm/AskFm.BLL/DTO/ActorDto.cs | 8 +++ AskFm/AskFm.BLL/DTO/NotificationDto.cs | 4 ++ AskFm/AskFm.BLL/DTO/PaginationDto.cs | 10 +++ .../Interfaces/INotificationRepository.cs | 6 +- AskFm/AskFm.DAL/Models/Notification.cs | 4 +- .../Repositories/NotificationRepository.cs | 64 +++++++++++++------ 7 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/ActorDto.cs create mode 100644 AskFm/AskFm.BLL/DTO/PaginationDto.cs diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index 2fcc00b..9980d2e 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -9,7 +9,6 @@ - diff --git a/AskFm/AskFm.BLL/DTO/ActorDto.cs b/AskFm/AskFm.BLL/DTO/ActorDto.cs new file mode 100644 index 0000000..029e938 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ActorDto.cs @@ -0,0 +1,8 @@ +namespace AskFm.BLL.DTO; + +public class ActorDto +{ + public int Id { get; set; } + public string Username { get; set; } + public string AvatarPath { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/NotificationDto.cs b/AskFm/AskFm.BLL/DTO/NotificationDto.cs index 2256cff..8b3c1fb 100644 --- a/AskFm/AskFm.BLL/DTO/NotificationDto.cs +++ b/AskFm/AskFm.BLL/DTO/NotificationDto.cs @@ -1,3 +1,5 @@ +using AskFm.BLL.DTO; + public class NotificationDto { public int Id { get; set; } @@ -10,4 +12,6 @@ public class NotificationDto public int ResourceId { get; set; } public int UserId { get; set; } + public ActorDto? Actor { get; set; } + } diff --git a/AskFm/AskFm.BLL/DTO/PaginationDto.cs b/AskFm/AskFm.BLL/DTO/PaginationDto.cs new file mode 100644 index 0000000..98c9a6b --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/PaginationDto.cs @@ -0,0 +1,10 @@ +namespace AskFm.BLL.DTO; + +public class PaginationDto +{ + public int CurrentPage { get; set; } + public int TotalPages { get; set; } + public int TotalCount { get; set; } + public bool HasNext { get; set; } + public bool HasPrevious { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs index e0d577c..5ab5546 100644 --- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -5,9 +5,11 @@ namespace AskFm.DAL.Interfaces; public interface INotificationRepository { - Task> GetAllNotifications(int userId, int pageNumber, int pageSize); + Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize); Task GetNotificationById(int notificationId); Task UpdateNotification(Notification notification); Task AddNotification(Notification notification); - Task> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); + Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); + Task MarkNotificationAsRead(int notificationId); + Task MarkAllNotificationsAsRead(int userId); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index 09469fb..0af0956 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -5,10 +5,12 @@ namespace AskFm.DAL.Models; public class Notification : ITrackable { - public NotificationStatus Type; + public NotificationStatus Type { get; set; } public int Id { get; set; } public int UserId { get; set; } public virtual ApplicationUser? User { get; set; } + public int? ActorUserId { get; set; } + public virtual ApplicationUser? ActorUser { get; set; } public bool isRead { get; set; } public int ResourceId { get; set; } diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs index 37a96f7..27c86b3 100644 --- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -8,29 +8,38 @@ namespace AskFm.DAL.Repositories; public class NotificationRepository : INotificationRepository { private readonly AppDbContext _context; + public NotificationRepository(AppDbContext context) { _context = context; } - public async Task> GetAllNotifications(int userId, int pageNumber, int pageSize) + public async Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize) { - var totalCount = await _context.Notifications.CountAsync(n => n.UserId == userId); - var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - var notifications = await _context.Notifications - .Where(n => n.UserId == userId) + var query = _context.Notifications + .Include(n => n.ActorUser) + .Where(n => n.UserId == userId); + + var totalCount = await query.CountAsync(); + + var notifications = await query .OrderByDescending(n => n.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); - return notifications; + + return (notifications, totalCount); } public async Task GetNotificationById(int notificationId) { - var notification = await _context.Notifications.FindAsync(notificationId); + var notification = await _context.Notifications + .Include(n => n.ActorUser) + .FirstOrDefaultAsync(n => n.Id == notificationId); + if (notification == null) throw new InvalidOperationException($"Notification with ID {notificationId} not found."); + return notification; } @@ -51,19 +60,38 @@ public async Task AddNotification(Notification notification) await _context.Notifications.AddAsync(notification); await _context.SaveChangesAsync(); } - public Task> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) + + public async Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) { - var query = _context.Notifications.AsQueryable(); - - if (userId > 0) - { - query = query.Where(n => n.UserId == userId); - } + var query = _context.Notifications + .Include(n => n.ActorUser) + .Where(n => n.UserId == userId && n.Type == status); - query = query.Where(n => n.Type == status); + var totalCount = await query.CountAsync(); + + var notifications = await query + .OrderByDescending(n => n.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + return (notifications, totalCount); + } - return Task.FromResult(query.OrderByDescending(n => n.CreatedAt) - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize)); + public async Task MarkNotificationAsRead(int notificationId) + { + var notification = await GetNotificationById(notificationId); + notification.isRead = true; + notification.UpdatedAt = DateTime.UtcNow; + await UpdateNotification(notification); + } + + public async Task MarkAllNotificationsAsRead(int userId) + { + await _context.Notifications + .Where(n => n.UserId == userId && !n.isRead) + .ExecuteUpdateAsync(n => n + .SetProperty(x => x.isRead, true) + .SetProperty(x => x.UpdatedAt, DateTime.UtcNow)); } } \ No newline at end of file From 0f1a29c2ec9e932068077509072a567ec96fca53 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Tue, 19 Aug 2025 03:37:31 +0300 Subject: [PATCH 24/92] Implement Notification Services --- AskFm/AskFm.BLL/AskFm.BLL.csproj | 5 +- .../DTO/NotificationCategoryResponse.cs | 8 ++ .../Services/INotificationService.cs | 13 +++ .../AskFm.BLL/Services/NotificationService.cs | 79 +++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs create mode 100644 AskFm/AskFm.BLL/Services/INotificationService.cs diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index 9980d2e..671c83a 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -8,10 +8,9 @@ - - - + + \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs b/AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs new file mode 100644 index 0000000..8bce4b7 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs @@ -0,0 +1,8 @@ +namespace AskFm.BLL.DTO; + +public class NotificationCategoryResponse +{ + public string Category { get; set; } + public List Notifications { get; set; } + public PaginationDto Pagination { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs new file mode 100644 index 0000000..5e31e8c --- /dev/null +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -0,0 +1,13 @@ +using AskFm.BLL.DTO; +using AskFm.DAL.Enums; + +namespace AskFm.BLL.Services; + +public interface INotificationService +{ + Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); + Task GetNotificationsByCategory(int userId, string category, int pageNumber = 1, int pageSize = 10); + Task MarkNotificationAsRead(int notificationId); + Task MarkAllNotificationsAsRead(int userId); + Task CreateNotification(int userId, int? actorUserId, NotificationStatus type, int resourceId, string message); +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index e69de29..84511ba 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -0,0 +1,79 @@ +using AskFm.BLL.DTO; +using AskFm.BLL.Services; +using AskFm.DAL.Enums; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using AutoMapper; + +public class NotificationService : INotificationService +{ + private readonly INotificationRepository _notificationRepository; + private readonly IMapper _mapper; + + public NotificationService(INotificationRepository notificationRepository, IMapper mapper) + { + _notificationRepository = notificationRepository; + _mapper = mapper; + } + + public async Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) + { + var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize); + return _mapper.Map>(notifications); + } + + public async Task GetNotificationsByCategory(int userId, string category, int pageNumber = 1, int pageSize = 10) + { + if (!Enum.TryParse(category, true, out var notificationType)) + throw new ArgumentException($"Invalid notification category: {category}"); + + var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); + + var notificationDtos = _mapper.Map>(notifications); + + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + + return new NotificationCategoryResponse + { + Category = category.ToUpper(), + Notifications = notificationDtos, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 + } + }; + } + + public async Task MarkNotificationAsRead(int notificationId) + { + await _notificationRepository.MarkNotificationAsRead(notificationId); + return "noti has been read"; + } + + public async Task MarkAllNotificationsAsRead(int userId) + { + await _notificationRepository.MarkAllNotificationsAsRead(userId); + return "All notifications marked as read"; + } + + public async Task CreateNotification(int userId, int? actorUserId, NotificationStatus type, int resourceId, string message) + { + var notification = new Notification + { + UserId = userId, + ActorUserId = actorUserId, + Type = type, + ResourceId = resourceId, + jsonContent = message, + isRead = false, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _notificationRepository.AddNotification(notification); + } +} \ No newline at end of file From e65c410b5d038a01670e32187d99ea7bfd88fe93 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Wed, 20 Aug 2025 02:33:02 +0300 Subject: [PATCH 25/92] fix: NotificationDto by adding PaginationDto and implement manual mapping --- AskFm/AskFm.BLL/DTO/NotificationDto.cs | 2 + ...esponse.cs => NotificationTypeResponse.cs} | 4 +- .../Services/INotificationService.cs | 2 +- .../AskFm.BLL/Services/NotificationService.cs | 60 +++++++++++++++---- 4 files changed, 55 insertions(+), 13 deletions(-) rename AskFm/AskFm.BLL/DTO/{NotificationCategoryResponse.cs => NotificationTypeResponse.cs} (62%) diff --git a/AskFm/AskFm.BLL/DTO/NotificationDto.cs b/AskFm/AskFm.BLL/DTO/NotificationDto.cs index 8b3c1fb..91c1fc3 100644 --- a/AskFm/AskFm.BLL/DTO/NotificationDto.cs +++ b/AskFm/AskFm.BLL/DTO/NotificationDto.cs @@ -13,5 +13,7 @@ public class NotificationDto public int ResourceId { get; set; } public int UserId { get; set; } public ActorDto? Actor { get; set; } + public PaginationDto Pagination { get; set; } + } diff --git a/AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs b/AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs similarity index 62% rename from AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs rename to AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs index 8bce4b7..b7ffd64 100644 --- a/AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs +++ b/AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs @@ -1,8 +1,8 @@ namespace AskFm.BLL.DTO; -public class NotificationCategoryResponse +public class NotificationTypeResponse { - public string Category { get; set; } + public string Type { get; set; } public List Notifications { get; set; } public PaginationDto Pagination { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs index 5e31e8c..d844f0a 100644 --- a/AskFm/AskFm.BLL/Services/INotificationService.cs +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -6,7 +6,7 @@ namespace AskFm.BLL.Services; public interface INotificationService { Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); - Task GetNotificationsByCategory(int userId, string category, int pageNumber = 1, int pageSize = 10); + Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); Task MarkNotificationAsRead(int notificationId); Task MarkAllNotificationsAsRead(int userId); Task CreateNotification(int userId, int? actorUserId, NotificationStatus type, int resourceId, string message); diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 84511ba..94e3eda 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -3,39 +3,79 @@ using AskFm.DAL.Enums; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; -using AutoMapper; public class NotificationService : INotificationService { private readonly INotificationRepository _notificationRepository; - private readonly IMapper _mapper; - public NotificationService(INotificationRepository notificationRepository, IMapper mapper) + public NotificationService(INotificationRepository notificationRepository) { _notificationRepository = notificationRepository; - _mapper = mapper; } public async Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) { var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize); - return _mapper.Map>(notifications); + + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + + var notificationDtos = notifications.Select(notification => new NotificationDto + { + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.jsonContent, + IsRead = notification.isRead, + CreatedAt = notification.CreatedAt, + Actor = new ActorDto + { + Id = notification.ActorUser?.Id ?? 0, + Username = notification.ActorUser?.UserName ?? "Unknown", + AvatarPath = notification.ActorUser?.AvatarPath ?? string.Empty + }, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 + } + }).ToList(); + + return notificationDtos; } - public async Task GetNotificationsByCategory(int userId, string category, int pageNumber = 1, int pageSize = 10) + public async Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) { if (!Enum.TryParse(category, true, out var notificationType)) throw new ArgumentException($"Invalid notification category: {category}"); var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); - var notificationDtos = _mapper.Map>(notifications); + var notificationDtos = notifications.Select(notification => new NotificationDto + { + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.jsonContent, + IsRead = notification.isRead, + CreatedAt = notification.CreatedAt, + Actor = new ActorDto + { + Id = notification.ActorUser?.Id ?? 0, + Username = notification.ActorUser?.UserName ?? "Unknown", + AvatarPath = notification.ActorUser?.AvatarPath ?? string.Empty + } + }).ToList(); var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - return new NotificationCategoryResponse + return new NotificationTypeResponse { - Category = category.ToUpper(), + Type = category.ToUpper(), Notifications = notificationDtos, Pagination = new PaginationDto { @@ -51,7 +91,7 @@ public async Task GetNotificationsByCategory(int u public async Task MarkNotificationAsRead(int notificationId) { await _notificationRepository.MarkNotificationAsRead(notificationId); - return "noti has been read"; + return "notification has been read"; } public async Task MarkAllNotificationsAsRead(int userId) From 74324033e330bb31116e2df145f66a7a6f5d4f7a Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Wed, 20 Aug 2025 21:46:56 +0300 Subject: [PATCH 26/92] Implement Notification Controller --- .../Controllers/NotificationController.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs index e69de29..ce55c73 100644 --- a/AskFm/AskFm.API/Controllers/NotificationController.cs +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -0,0 +1,103 @@ +using AskFm.BLL.Services; +using AskFm.DAL.Enums; +using Microsoft.AspNetCore.Mvc; + +namespace AskFm.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class NotificationController : ControllerBase + { + private readonly INotificationService _notificationService; + + public NotificationController(INotificationService notificationService) + { + _notificationService = notificationService; + } + + [HttpGet("{userId}")] + public async Task GetUserNotifications(int userId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + { + try + { + var notifications = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); + return Ok(notifications); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpGet("{userId}/type/{category}")] + public async Task GetNotificationsByType(int userId, string category, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + { + try + { + var response = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); + return Ok(response); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + catch (Exception ex) + { + return StatusCode(500, ex.Message); + } + } + + [HttpPut("{notificationId}/read")] + public async Task MarkNotificationAsRead(int notificationId) + { + try + { + var result = await _notificationService.MarkNotificationAsRead(notificationId); + return Ok(new { message = result }); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPut("{userId}/read-all")] + public async Task MarkAllNotificationsAsRead(int userId) + { + try + { + var result = await _notificationService.MarkAllNotificationsAsRead(userId); + return Ok(new { message = result }); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPost] + public async Task CreateNotification( + int userId, + int? actorUserId, + NotificationStatus type, + int resourceId, + string message) + { + try + { + await _notificationService.CreateNotification( + userId, + actorUserId, + type, + resourceId, + message); + + return CreatedAtAction(nameof(GetUserNotifications), new { userId = userId }, new { message = "Notification created successfully" }); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + } +} \ No newline at end of file From 9125016e756b99531e0d9c8f66d53014e5b4ef3b Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Wed, 20 Aug 2025 22:18:11 +0300 Subject: [PATCH 27/92] Implement notification hub --- AskFm/AskFm.BLL/Hub/NotificationHub.cs | 20 +++++++++++++----- .../AskFm.BLL/Services/NotificationService.cs | 21 ++++++++++++++++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/AskFm/AskFm.BLL/Hub/NotificationHub.cs b/AskFm/AskFm.BLL/Hub/NotificationHub.cs index 4b98ecb..e881fcd 100644 --- a/AskFm/AskFm.BLL/Hub/NotificationHub.cs +++ b/AskFm/AskFm.BLL/Hub/NotificationHub.cs @@ -1,12 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; namespace AskFm.BLL.Hub { public class NotificationHub : Microsoft.AspNetCore.SignalR.Hub { - + public async Task JoinUserGroup(string userId) + { + await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}"); + } + + public async Task LeaveUserGroup(string userId) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user_{userId}"); + } + + public override async Task OnDisconnectedAsync(Exception exception) + { + await base.OnDisconnectedAsync(exception); + } } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 94e3eda..3a2fb52 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -1,16 +1,21 @@ using AskFm.BLL.DTO; +using AskFm.BLL.Hub; using AskFm.BLL.Services; using AskFm.DAL.Enums; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; +using Microsoft.AspNetCore.SignalR; public class NotificationService : INotificationService { private readonly INotificationRepository _notificationRepository; + private readonly IHubContext _hubContext; - public NotificationService(INotificationRepository notificationRepository) + + public NotificationService(INotificationRepository notificationRepository, IHubContext hubContext) { _notificationRepository = notificationRepository; + _hubContext = hubContext; } public async Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) @@ -115,5 +120,19 @@ public async Task CreateNotification(int userId, int? actorUserId, NotificationS }; await _notificationRepository.AddNotification(notification); + + var notificationDto = new NotificationDto + { + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.jsonContent, + IsRead = notification.isRead, + CreatedAt = notification.CreatedAt + }; + + await _hubContext.Clients.Group($"user_{userId}") + .SendAsync("ReceiveNotification", notificationDto); } } \ No newline at end of file From 065fdacb320c1a44230a93007873ce18f09c6142 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Wed, 20 Aug 2025 22:26:20 +0300 Subject: [PATCH 28/92] Add Dependency injection --- AskFm/AskFm.API/Program.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 46be5c6..c07d4a8 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -7,6 +7,8 @@ using DotNetEnv; using Microsoft.EntityFrameworkCore.Proxies; using AskFm.BLL.Hub; +using AskFm.BLL.Services; + namespace AskFm.API; @@ -40,6 +42,8 @@ public static void Main(string[] args) builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddSignalR(); From 09d1d0c46572b79315e98171eff833d76e0eddae Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Fri, 22 Aug 2025 18:08:27 +0300 Subject: [PATCH 29/92] Provide JWT configuration --- AskFm/AskFm.API/AskFm.API.csproj | 1 + AskFm/AskFm.API/Program.cs | 33 +++++++++++++++++++ AskFm/AskFm.BLL/AskFm.BLL.csproj | 4 +++ AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs | 3 +- .../UserIdentityService/IUserService.cs | 2 +- AskFm/Tests/Tests.csproj | 12 +++++++ 6 files changed, 53 insertions(+), 2 deletions(-) diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index c71e10c..f7ee7c4 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -8,6 +8,7 @@ + diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 1da703b..46dde24 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -1,3 +1,4 @@ +using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; using AskFm.DAL; @@ -5,8 +6,13 @@ using AskFm.DAL.Interfaces; using AskFm.DAL.Repositories; using DotNetEnv; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore.Proxies; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using AskFm.BLL.Services.UserIdentityService; +using Castle.Components.DictionaryAdapter.Xml; namespace AskFm.API; @@ -44,6 +50,33 @@ public static void Main(string[] args) builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); + + JwtOptions jwtOptions = new JwtOptions + { + Issuer = Environment.GetEnvironmentVariable("ISSUER"), + Audience = Environment.GetEnvironmentVariable("AUDIENCE"), + SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"), + }; + if (jwtOptions == null) + { + throw new Exception("jwtOptions is null"); + } + + Console.WriteLine(jwtOptions.Issuer + " " + jwtOptions.Audience + " " + jwtOptions.SigningKey); + builder.Services.AddAuthentication() + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, Options => + { + Options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtOptions.Issuer, + ValidateAudience = true, + ValidAudience = jwtOptions.Audience, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)) + }; + + }); var app = builder.Build(); diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index ba85168..6977b2a 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs index 01913c2..b4e215e 100644 --- a/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs @@ -2,5 +2,6 @@ namespace AskFm.BLL.DTO.UserDTOs; public class LoginDTO { - + public string Email { get; set; } + public string Password { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs index 790adf4..9f861e5 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -10,4 +10,4 @@ public interface IUserService Task> UnfollowUserAsync(int followerId, int targetUserId); Task UpdateLastSeenAsync(int userId, DateTime lastSeen); Task GetUserByIdAsync(int userId); -} +} \ No newline at end of file diff --git a/AskFm/Tests/Tests.csproj b/AskFm/Tests/Tests.csproj index 95d873d..f4f2cfa 100644 --- a/AskFm/Tests/Tests.csproj +++ b/AskFm/Tests/Tests.csproj @@ -10,6 +10,7 @@ + @@ -18,4 +19,15 @@ + + + + + + + + + + + From b701906ca858aa7dd201373d09cb29d595dc8c21 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Fri, 22 Aug 2025 18:10:21 +0300 Subject: [PATCH 30/92] JWT Options --- AskFm/AskFm.API/AskFm.API.csproj | 1 - AskFm/AskFm.API/Program.cs | 33 ----------------- AskFm/AskFm.BLL/AskFm.BLL.csproj | 4 --- AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs | 3 +- .../UserIdentityService/AuthService.cs | 26 ++++++++++++++ .../UserIdentityService/IUserService.cs | 2 +- .../UserIdentityService/JwtOptions.cs | 8 +++++ .../UserIdentityService/UserService.cs | 36 +++++++++++++++++++ AskFm/Tests/Tests.csproj | 12 ------- 9 files changed, 72 insertions(+), 53 deletions(-) create mode 100644 AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs create mode 100644 AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs create mode 100644 AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index f7ee7c4..c71e10c 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -8,7 +8,6 @@ - diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 46dde24..1da703b 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -1,4 +1,3 @@ -using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; using AskFm.DAL; @@ -6,13 +5,8 @@ using AskFm.DAL.Interfaces; using AskFm.DAL.Repositories; using DotNetEnv; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore.Proxies; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; -using AskFm.BLL.Services.UserIdentityService; -using Castle.Components.DictionaryAdapter.Xml; namespace AskFm.API; @@ -50,33 +44,6 @@ public static void Main(string[] args) builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); - - JwtOptions jwtOptions = new JwtOptions - { - Issuer = Environment.GetEnvironmentVariable("ISSUER"), - Audience = Environment.GetEnvironmentVariable("AUDIENCE"), - SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"), - }; - if (jwtOptions == null) - { - throw new Exception("jwtOptions is null"); - } - - Console.WriteLine(jwtOptions.Issuer + " " + jwtOptions.Audience + " " + jwtOptions.SigningKey); - builder.Services.AddAuthentication() - .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, Options => - { - Options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = jwtOptions.Issuer, - ValidateAudience = true, - ValidAudience = jwtOptions.Audience, - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)) - }; - - }); var app = builder.Build(); diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index 6977b2a..ba85168 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -11,8 +11,4 @@ - - - - diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs index b4e215e..01913c2 100644 --- a/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs @@ -2,6 +2,5 @@ namespace AskFm.BLL.DTO.UserDTOs; public class LoginDTO { - public string Email { get; set; } - public string Password { get; set; } + } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs new file mode 100644 index 0000000..3770009 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs @@ -0,0 +1,26 @@ +using AskFm.BLL.DTO.UserDTOs; + +namespace AskFm.BLL.Services.UserIdentityService; + +public class AuthService : IAuthService +{ + public Task> LoginAsync(LoginDTO request) + { + throw new NotImplementedException(); + } + + public Task> RegisterAsync(RegisterUserDTO request) + { + throw new NotImplementedException(); + } + + public Task> RevokeRefreshTokenAsync(string refreshToken) + { + throw new NotImplementedException(); + } + + public Task> RevokeAllRefreshTokensAsync(int userId) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs index 9f861e5..790adf4 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -10,4 +10,4 @@ public interface IUserService Task> UnfollowUserAsync(int followerId, int targetUserId); Task UpdateLastSeenAsync(int userId, DateTime lastSeen); Task GetUserByIdAsync(int userId); -} \ No newline at end of file +} diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs new file mode 100644 index 0000000..57c15fa --- /dev/null +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs @@ -0,0 +1,8 @@ +namespace AskFm.BLL.Services.UserIdentityService; + +public class JwtOptions +{ + public string Issuer { get; set; } + public string Audience { get; set; } + public string SigningKey { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs new file mode 100644 index 0000000..fc7823c --- /dev/null +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -0,0 +1,36 @@ +using AskFm.BLL.DTO.UserDTOs; + +namespace AskFm.BLL.Services.UserIdentityService; + +public class UserService : IUserService +{ + public Task> UpdateUserAsync(int userId, UpdateUserDTO updatedUser) + { + throw new NotImplementedException(); + } + + public Task> DeleteUserAsync(int userId) + { + throw new NotImplementedException(); + } + + public Task> FollowUserAsync(int followerId, int targetUserId) + { + throw new NotImplementedException(); + } + + public Task> UnfollowUserAsync(int followerId, int targetUserId) + { + throw new NotImplementedException(); + } + + public Task UpdateLastSeenAsync(int userId, DateTime lastSeen) + { + throw new NotImplementedException(); + } + + public Task GetUserByIdAsync(int userId) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/AskFm/Tests/Tests.csproj b/AskFm/Tests/Tests.csproj index f4f2cfa..95d873d 100644 --- a/AskFm/Tests/Tests.csproj +++ b/AskFm/Tests/Tests.csproj @@ -10,7 +10,6 @@ - @@ -19,15 +18,4 @@ - - - - - - - - - - - From cd2c95707342091ba90861a0507dcaa6c12cb8bc Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Fri, 22 Aug 2025 19:43:01 +0300 Subject: [PATCH 31/92] idenitity options --- AskFm/AskFm.API/Program.cs | 66 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 1da703b..025c86d 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -1,3 +1,4 @@ +using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; using AskFm.DAL; @@ -5,8 +6,13 @@ using AskFm.DAL.Interfaces; using AskFm.DAL.Repositories; using DotNetEnv; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore.Proxies; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using AskFm.BLL.Services.UserIdentityService; +using Castle.Components.DictionaryAdapter.Xml; namespace AskFm.API; @@ -35,15 +41,69 @@ public static void Main(string[] args) .UseLazyLoadingProxies() .UseSqlServer(ConnectionString)); - builder.Services.AddIdentity>() - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); + builder.Services.AddScoped(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); + + JwtOptions jwtOptions = new JwtOptions + { + Issuer = Environment.GetEnvironmentVariable("ISSUER"), + Audience = Environment.GetEnvironmentVariable("AUDIENCE"), + SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"), + }; + if (jwtOptions == null) + { + throw new Exception("jwtOptions is null"); + } + + Console.WriteLine(jwtOptions.Issuer + " " + jwtOptions.Audience + " " + jwtOptions.SigningKey); + builder.Services.AddAuthentication() + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, Options => + { + Options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtOptions.Issuer, + ValidateAudience = true, + ValidAudience = jwtOptions.Audience, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)) + }; + + }); + + builder.Services.AddIdentity>(options => + { + //password configuration + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireUppercase = true; + options.Password.RequireNonAlphanumeric = true; + options.Password.RequiredLength = 8; + + //Email + options.User.RequireUniqueEmail = true; + + // Lockout + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); + options.Lockout.AllowedForNewUsers = true; + + // sign in options + options.SignIn.RequireConfirmedEmail = false; + options.SignIn.RequireConfirmedAccount = false; + options.SignIn.RequireConfirmedPhoneNumber = false; + /* + * close confirmed email imediatly in register, + * but in other scenario we will block some action untill the user verify his email + */ + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); var app = builder.Build(); From b2dca6d2f7a7687282475bd213876e9618d2f64f Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 24 Aug 2025 20:22:55 +0300 Subject: [PATCH 32/92] refactor: Remove redundant actor fields and resolve actors via ResourceId, Update Service methods to use new approach --- .../Controllers/NotificationController.cs | 14 +-- .../Services/INotificationService.cs | 2 +- .../AskFm.BLL/Services/NotificationService.cs | 95 +++++++++++-------- .../Interfaces/INotificationRepository.cs | 1 + AskFm/AskFm.DAL/Models/Notification.cs | 2 - .../Repositories/NotificationRepository.cs | 67 ++++++++++--- 6 files changed, 112 insertions(+), 69 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs index ce55c73..5630476 100644 --- a/AskFm/AskFm.API/Controllers/NotificationController.cs +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -76,21 +76,11 @@ public async Task MarkAllNotificationsAsRead(int userId) } [HttpPost] - public async Task CreateNotification( - int userId, - int? actorUserId, - NotificationStatus type, - int resourceId, - string message) + public async Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message) { try { - await _notificationService.CreateNotification( - userId, - actorUserId, - type, - resourceId, - message); + await _notificationService.CreateNotification(userId, type, resourceId, message); return CreatedAtAction(nameof(GetUserNotifications), new { userId = userId }, new { message = "Notification created successfully" }); } diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs index d844f0a..53955a4 100644 --- a/AskFm/AskFm.BLL/Services/INotificationService.cs +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -9,5 +9,5 @@ public interface INotificationService Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); Task MarkNotificationAsRead(int notificationId); Task MarkAllNotificationsAsRead(int userId); - Task CreateNotification(int userId, int? actorUserId, NotificationStatus type, int resourceId, string message); + Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 3a2fb52..174b81c 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -21,61 +21,73 @@ public NotificationService(INotificationRepository notificationRepository, IHubC public async Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) { var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize); - + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - var notificationDtos = notifications.Select(notification => new NotificationDto + var notificationDtos = new List(); + + foreach (var notification in notifications) { - Id = notification.Id, - UserId = notification.UserId, - Type = notification.Type.ToString(), - ResourceId = notification.ResourceId, - Message = notification.jsonContent, - IsRead = notification.isRead, - CreatedAt = notification.CreatedAt, - Actor = new ActorDto - { - Id = notification.ActorUser?.Id ?? 0, - Username = notification.ActorUser?.UserName ?? "Unknown", - AvatarPath = notification.ActorUser?.AvatarPath ?? string.Empty - }, - Pagination = new PaginationDto + var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); + notificationDtos.Add(new NotificationDto { - CurrentPage = pageNumber, - TotalPages = totalPages, - TotalCount = totalCount, - HasNext = pageNumber < totalPages, - HasPrevious = pageNumber > 1 - } - }).ToList(); - + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.jsonContent, + IsRead = notification.isRead, + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser?.UserName ?? "Unknown", + AvatarPath = actorUser?.AvatarPath ?? String.Empty + }, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 + } + }); + } return notificationDtos; } public async Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) { + // first we have to convert category to upper case and match it with enum if (!Enum.TryParse(category, true, out var notificationType)) throw new ArgumentException($"Invalid notification category: {category}"); var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); - var notificationDtos = notifications.Select(notification => new NotificationDto + var notificationDtos = new List(); + + foreach (var notification in notifications) { - Id = notification.Id, - UserId = notification.UserId, - Type = notification.Type.ToString(), - ResourceId = notification.ResourceId, - Message = notification.jsonContent, - IsRead = notification.isRead, - CreatedAt = notification.CreatedAt, - Actor = new ActorDto - { - Id = notification.ActorUser?.Id ?? 0, - Username = notification.ActorUser?.UserName ?? "Unknown", - AvatarPath = notification.ActorUser?.AvatarPath ?? string.Empty - } - }).ToList(); - + var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); + notificationDtos.Add(new NotificationDto + { + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.jsonContent, + IsRead = notification.isRead, + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser?.UserName ?? "Unknown", + AvatarPath = actorUser?.AvatarPath ?? String.Empty + } + }); + } + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); return new NotificationTypeResponse @@ -105,12 +117,11 @@ public async Task MarkAllNotificationsAsRead(int userId) return "All notifications marked as read"; } - public async Task CreateNotification(int userId, int? actorUserId, NotificationStatus type, int resourceId, string message) + public async Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message) { var notification = new Notification { UserId = userId, - ActorUserId = actorUserId, Type = type, ResourceId = resourceId, jsonContent = message, diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs index 5ab5546..b59ba3b 100644 --- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -12,4 +12,5 @@ public interface INotificationRepository Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); Task MarkNotificationAsRead(int notificationId); Task MarkAllNotificationsAsRead(int userId); + Task GetActorUserByResourceId(int resourceId, NotificationStatus type); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index 0af0956..799ba14 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -9,8 +9,6 @@ public class Notification : ITrackable public int Id { get; set; } public int UserId { get; set; } public virtual ApplicationUser? User { get; set; } - public int? ActorUserId { get; set; } - public virtual ApplicationUser? ActorUser { get; set; } public bool isRead { get; set; } public int ResourceId { get; set; } diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs index 27c86b3..29e9cfa 100644 --- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -16,9 +16,7 @@ public NotificationRepository(AppDbContext context) public async Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize) { - var query = _context.Notifications - .Include(n => n.ActorUser) - .Where(n => n.UserId == userId); + var query = _context.Notifications.Where(n => n.UserId == userId); var totalCount = await query.CountAsync(); @@ -33,9 +31,7 @@ public NotificationRepository(AppDbContext context) public async Task GetNotificationById(int notificationId) { - var notification = await _context.Notifications - .Include(n => n.ActorUser) - .FirstOrDefaultAsync(n => n.Id == notificationId); + var notification = await _context.Notifications.FirstOrDefaultAsync(n => n.Id == notificationId); if (notification == null) throw new InvalidOperationException($"Notification with ID {notificationId} not found."); @@ -63,9 +59,7 @@ public async Task AddNotification(Notification notification) public async Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) { - var query = _context.Notifications - .Include(n => n.ActorUser) - .Where(n => n.UserId == userId && n.Type == status); + var query = _context.Notifications.Where(n => n.UserId == userId && n.Type == status); var totalCount = await query.CountAsync(); @@ -82,7 +76,7 @@ public async Task MarkNotificationAsRead(int notificationId) { var notification = await GetNotificationById(notificationId); notification.isRead = true; - notification.UpdatedAt = DateTime.UtcNow; + //notification.UpdatedAt = DateTime.UtcNow; await UpdateNotification(notification); } @@ -91,7 +85,56 @@ public async Task MarkAllNotificationsAsRead(int userId) await _context.Notifications .Where(n => n.UserId == userId && !n.isRead) .ExecuteUpdateAsync(n => n - .SetProperty(x => x.isRead, true) - .SetProperty(x => x.UpdatedAt, DateTime.UtcNow)); + .SetProperty(x => x.isRead, true)); + // .SetProperty(x => x.UpdatedAt, DateTime.UtcNow)) + } + + public async Task GetActorUserByResourceId(int resourceId, NotificationStatus type) + { + // In follow case the follow model does not have a follow id so we use the followedId to get the actor user + if (type == NotificationStatus.FOLLOW) + { + return await _context.Follows + .Where(f => f.FollowedId == resourceId) + .Select(f => f.Follower) + .FirstOrDefaultAsync(); + } + else if (type == NotificationStatus.QUESTION) + { + return await _context.Threads + .Where(t => t.Id == resourceId) + .Select(t => t.Asker) + .FirstOrDefaultAsync(); + } + else if (type == NotificationStatus.ANSWER) + { + return await _context.Threads + .Where(t => t.Id == resourceId) + .Select(t => t.Asked) + .FirstOrDefaultAsync(); + } + else if (type == NotificationStatus.COMMENT_LIKE) + { + return await _context.CommentLikes + .Where(cl => cl.CommentId == resourceId) + .Select(cl => cl.User) + .FirstOrDefaultAsync(); + } + else if (type == NotificationStatus.QUESTION_LIKE) + { + return await _context.ThreadLikes + .Where(tl => tl.ThreadId == resourceId) + .Select(tl => tl.User) + .FirstOrDefaultAsync(); + } + else if (type == NotificationStatus.REPLAY) + { + return await _context.Comments + .Where(c => c.Id == resourceId) + .Select(c => c.User) + .FirstOrDefaultAsync(); + } + return null; + } } \ No newline at end of file From fda6755c60b9a600c6cb8146565e5e2099bac691 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 24 Aug 2025 21:00:02 +0300 Subject: [PATCH 33/92] Remove NotificationResponseType that's because the frontend already sent the type of notification --- .../AskFm.BLL/DTO/NotificationTypeResponse.cs | 8 ---- .../Services/INotificationService.cs | 2 +- .../AskFm.BLL/Services/NotificationService.cs | 37 ++++++++----------- 3 files changed, 17 insertions(+), 30 deletions(-) delete mode 100644 AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs diff --git a/AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs b/AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs deleted file mode 100644 index b7ffd64..0000000 --- a/AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AskFm.BLL.DTO; - -public class NotificationTypeResponse -{ - public string Type { get; set; } - public List Notifications { get; set; } - public PaginationDto Pagination { get; set; } -} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs index 53955a4..4eb9d63 100644 --- a/AskFm/AskFm.BLL/Services/INotificationService.cs +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -6,7 +6,7 @@ namespace AskFm.BLL.Services; public interface INotificationService { Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); - Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); + Task> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); Task MarkNotificationAsRead(int notificationId); Task MarkAllNotificationsAsRead(int userId); Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message); diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 174b81c..ad11a3e 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -57,21 +57,23 @@ public async Task> GetUserNotifications(int userId, int pa return notificationDtos; } - public async Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) + public async Task> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) { - // first we have to convert category to upper case and match it with enum - if (!Enum.TryParse(category, true, out var notificationType)) + // Convert category to uppercase and match with enum + if (!Enum.TryParse(category.ToUpper(), out var notificationType)) throw new ArgumentException($"Invalid notification category: {category}"); var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); - + + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var notificationDtos = new List(); foreach (var notification in notifications) { var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); notificationDtos.Add(new NotificationDto - { + { Id = notification.Id, UserId = notification.UserId, Type = notification.Type.ToString(), @@ -84,27 +86,20 @@ public async Task GetNotificationsByType(int userId, s Id = actorUser.Id, Username = actorUser?.UserName ?? "Unknown", AvatarPath = actorUser?.AvatarPath ?? String.Empty + }, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 } }); } - var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - - return new NotificationTypeResponse - { - Type = category.ToUpper(), - Notifications = notificationDtos, - Pagination = new PaginationDto - { - CurrentPage = pageNumber, - TotalPages = totalPages, - TotalCount = totalCount, - HasNext = pageNumber < totalPages, - HasPrevious = pageNumber > 1 - } - }; + return notificationDtos; } - public async Task MarkNotificationAsRead(int notificationId) { await _notificationRepository.MarkNotificationAsRead(notificationId); From bf3cefd68c0923b2e03cab925477e643d75edf87 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 24 Aug 2025 22:08:51 +0300 Subject: [PATCH 34/92] Refactor notifications to use UnitOfWork and Repository pattern --- .../AskFm.BLL/Services/NotificationService.cs | 26 ++++- .../Interfaces/INotificationRepository.cs | 5 - AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs | 3 +- .../Repositories/NotificationRepository.cs | 97 ++++--------------- AskFm/AskFm.DAL/UnitOfWork.cs | 8 +- 5 files changed, 46 insertions(+), 93 deletions(-) diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index ad11a3e..81ff78f 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -9,12 +9,14 @@ public class NotificationService : INotificationService { private readonly INotificationRepository _notificationRepository; + private readonly IUnitOfWork _unitOfWork; private readonly IHubContext _hubContext; - public NotificationService(INotificationRepository notificationRepository, IHubContext hubContext) + public NotificationService(INotificationRepository notificationRepository, IUnitOfWork unitOfWork, IHubContext hubContext) { _notificationRepository = notificationRepository; + _unitOfWork = unitOfWork; _hubContext = hubContext; } @@ -102,13 +104,28 @@ public async Task> GetNotificationsByType(int userId, stri } public async Task MarkNotificationAsRead(int notificationId) { - await _notificationRepository.MarkNotificationAsRead(notificationId); + var notification = await _unitOfWork.Notifications.GetByIdAsync(notificationId); + if (notification == null) + throw new InvalidOperationException($"Notification with ID {notificationId} not found."); + + notification.isRead = true; + _unitOfWork.Notifications.Update(notification); + await _unitOfWork.SaveAsync(); + return "notification has been read"; } public async Task MarkAllNotificationsAsRead(int userId) { - await _notificationRepository.MarkAllNotificationsAsRead(userId); + var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.isRead); + + foreach (var notification in unreadNotifications) + { + notification.isRead = true; + _unitOfWork.Notifications.Update(notification); + } + + await _unitOfWork.SaveAsync(); return "All notifications marked as read"; } @@ -125,7 +142,8 @@ public async Task CreateNotification(int userId, NotificationStatus type, int re UpdatedAt = DateTime.UtcNow }; - await _notificationRepository.AddNotification(notification); + await _unitOfWork.Notifications.AddAsync(notification); + await _unitOfWork.SaveAsync(); var notificationDto = new NotificationDto { diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs index b59ba3b..816c2f1 100644 --- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -6,11 +6,6 @@ namespace AskFm.DAL.Interfaces; public interface INotificationRepository { Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize); - Task GetNotificationById(int notificationId); - Task UpdateNotification(Notification notification); - Task AddNotification(Notification notification); Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); - Task MarkNotificationAsRead(int notificationId); - Task MarkAllNotificationsAsRead(int userId); Task GetActorUserByResourceId(int resourceId, NotificationStatus type); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs index 108f184..e50167d 100644 --- a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs +++ b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs @@ -1,12 +1,11 @@ using AskFm.DAL.Models; -using Thread = System.Threading.Thread; namespace AskFm.DAL.Interfaces; public interface IUnitOfWork : IDisposable { IRepository Users { get; } - IRepository Threads { get; } + IRepository Threads { get; } IRepository SavedThreads { get; } IRepository ThreadLikes { get; } IRepository Comments { get; } diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs index 29e9cfa..d49564d 100644 --- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -7,16 +7,16 @@ namespace AskFm.DAL.Repositories; public class NotificationRepository : INotificationRepository { - private readonly AppDbContext _context; - - public NotificationRepository(AppDbContext context) + private readonly IUnitOfWork _unitOfWork; + + public NotificationRepository(IUnitOfWork unitOfWork) { - _context = context; + _unitOfWork = unitOfWork; } public async Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize) { - var query = _context.Notifications.Where(n => n.UserId == userId); + var query = _unitOfWork.Notifications.FindAll(n => n.UserId == userId); var totalCount = await query.CountAsync(); @@ -28,39 +28,11 @@ public NotificationRepository(AppDbContext context) return (notifications, totalCount); } - - public async Task GetNotificationById(int notificationId) - { - var notification = await _context.Notifications.FirstOrDefaultAsync(n => n.Id == notificationId); - - if (notification == null) - throw new InvalidOperationException($"Notification with ID {notificationId} not found."); - - return notification; - } - - public async Task UpdateNotification(Notification notification) - { - if (notification == null) - throw new ArgumentNullException(nameof(notification)); - - _context.Notifications.Update(notification); - await _context.SaveChangesAsync(); - } - - public async Task AddNotification(Notification notification) - { - if (notification == null) - throw new ArgumentNullException(nameof(notification)); - - await _context.Notifications.AddAsync(notification); - await _context.SaveChangesAsync(); - } public async Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) { - var query = _context.Notifications.Where(n => n.UserId == userId && n.Type == status); - + var query = _unitOfWork.Notifications.FindAll(n => n.UserId == userId && n.Type == status); + var totalCount = await query.CountAsync(); var notifications = await query @@ -72,69 +44,38 @@ public async Task AddNotification(Notification notification) return (notifications, totalCount); } - public async Task MarkNotificationAsRead(int notificationId) - { - var notification = await GetNotificationById(notificationId); - notification.isRead = true; - //notification.UpdatedAt = DateTime.UtcNow; - await UpdateNotification(notification); - } - - public async Task MarkAllNotificationsAsRead(int userId) - { - await _context.Notifications - .Where(n => n.UserId == userId && !n.isRead) - .ExecuteUpdateAsync(n => n - .SetProperty(x => x.isRead, true)); - // .SetProperty(x => x.UpdatedAt, DateTime.UtcNow)) - } - public async Task GetActorUserByResourceId(int resourceId, NotificationStatus type) { - // In follow case the follow model does not have a follow id so we use the followedId to get the actor user if (type == NotificationStatus.FOLLOW) { - return await _context.Follows - .Where(f => f.FollowedId == resourceId) - .Select(f => f.Follower) - .FirstOrDefaultAsync(); + var follow = await _unitOfWork.Follows.FindAsync(f => f.FollowedId == resourceId, new[] { "Follower" }); + return follow?.Follower; } else if (type == NotificationStatus.QUESTION) { - return await _context.Threads - .Where(t => t.Id == resourceId) - .Select(t => t.Asker) - .FirstOrDefaultAsync(); + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == resourceId, new[] { "Asker" }); + return thread?.Asker; } else if (type == NotificationStatus.ANSWER) { - return await _context.Threads - .Where(t => t.Id == resourceId) - .Select(t => t.Asked) - .FirstOrDefaultAsync(); + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == resourceId, new[] { "Asked" }); + return thread?.Asked; } else if (type == NotificationStatus.COMMENT_LIKE) { - return await _context.CommentLikes - .Where(cl => cl.CommentId == resourceId) - .Select(cl => cl.User) - .FirstOrDefaultAsync(); + var commentLike = await _unitOfWork.CommentLikes.FindAsync(cl => cl.CommentId == resourceId, new[] { "User" }); + return commentLike?.User; } else if (type == NotificationStatus.QUESTION_LIKE) { - return await _context.ThreadLikes - .Where(tl => tl.ThreadId == resourceId) - .Select(tl => tl.User) - .FirstOrDefaultAsync(); + var threadLike = await _unitOfWork.ThreadLikes.FindAsync(tl => tl.ThreadId == resourceId, new[] { "User" }); + return threadLike?.User; } else if (type == NotificationStatus.REPLAY) { - return await _context.Comments - .Where(c => c.Id == resourceId) - .Select(c => c.User) - .FirstOrDefaultAsync(); + var comment = await _unitOfWork.Comments.FindAsync(c => c.Id == resourceId, new[] { "User" }); + return comment?.User; } return null; - } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/UnitOfWork.cs b/AskFm/AskFm.DAL/UnitOfWork.cs index 573f3ae..396696c 100644 --- a/AskFm/AskFm.DAL/UnitOfWork.cs +++ b/AskFm/AskFm.DAL/UnitOfWork.cs @@ -1,7 +1,6 @@ using AskFm.DAL.Interfaces; using AskFm.DAL.Models; using AskFm.DAL.Repositories; -using Thread = System.Threading.Thread; namespace AskFm.DAL; @@ -10,7 +9,7 @@ public class UnitOfWork : IUnitOfWork private readonly AppDbContext _context; private IRepository _users; - private IRepository _threads; + private IRepository _threads; private IRepository _savedThreads; private IRepository _threadLikes; private IRepository _comments; @@ -36,12 +35,12 @@ public IRepository Users } } - public IRepository Threads { + public IRepository Threads { get { if (_threads == null) { - _threads = new Repository(_context); + _threads = new Repository(_context); } return _threads; } @@ -119,6 +118,7 @@ public IRepository Notifications } } + IRepository IUnitOfWork.Threads => throw new NotImplementedException(); public void Dispose() { From d7bd9cfc14f318a663d561c0b9ba1039b6e9b362 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 24 Aug 2025 23:50:44 +0300 Subject: [PATCH 35/92] Update Naming in Notification model and apply migrations for it --- .../AskFm.BLL/Services/NotificationService.cs | 22 +- ...204202_NotificationModelNaming.Designer.cs | 763 ++++++++++++++++++ .../20250824204202_NotificationModelNaming.cs | 49 ++ .../Migrations/AppDbContextModelSnapshot.cs | 20 +- AskFm/AskFm.DAL/Models/Notification.cs | 4 +- .../NotificationConfigration.cs | 4 +- 6 files changed, 840 insertions(+), 22 deletions(-) create mode 100644 AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 81ff78f..d94ea4f 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -37,8 +37,8 @@ public async Task> GetUserNotifications(int userId, int pa UserId = notification.UserId, Type = notification.Type.ToString(), ResourceId = notification.ResourceId, - Message = notification.jsonContent, - IsRead = notification.isRead, + Message = notification.Message, + IsRead = notification.IsRead, CreatedAt = notification.CreatedAt, Actor = actorUser == null ? null : new ActorDto { @@ -80,8 +80,8 @@ public async Task> GetNotificationsByType(int userId, stri UserId = notification.UserId, Type = notification.Type.ToString(), ResourceId = notification.ResourceId, - Message = notification.jsonContent, - IsRead = notification.isRead, + Message = notification.Message, + IsRead = notification.IsRead, CreatedAt = notification.CreatedAt, Actor = actorUser == null ? null : new ActorDto { @@ -108,7 +108,7 @@ public async Task MarkNotificationAsRead(int notificationId) if (notification == null) throw new InvalidOperationException($"Notification with ID {notificationId} not found."); - notification.isRead = true; + notification.IsRead = true; _unitOfWork.Notifications.Update(notification); await _unitOfWork.SaveAsync(); @@ -117,11 +117,11 @@ public async Task MarkNotificationAsRead(int notificationId) public async Task MarkAllNotificationsAsRead(int userId) { - var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.isRead); + var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.IsRead); foreach (var notification in unreadNotifications) { - notification.isRead = true; + notification.IsRead = true; _unitOfWork.Notifications.Update(notification); } @@ -136,8 +136,8 @@ public async Task CreateNotification(int userId, NotificationStatus type, int re UserId = userId, Type = type, ResourceId = resourceId, - jsonContent = message, - isRead = false, + Message = message, + IsRead = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; @@ -151,8 +151,8 @@ public async Task CreateNotification(int userId, NotificationStatus type, int re UserId = notification.UserId, Type = notification.Type.ToString(), ResourceId = notification.ResourceId, - Message = notification.jsonContent, - IsRead = notification.isRead, + Message = notification.Message, + IsRead = notification.IsRead, CreatedAt = notification.CreatedAt }; diff --git a/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.Designer.cs new file mode 100644 index 0000000..ddd28f5 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.Designer.cs @@ -0,0 +1,763 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250824204202_NotificationModelNaming")] + partial class NotificationModelNaming + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs b/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs new file mode 100644 index 0000000..38fee4d --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class NotificationModelNaming : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "isRead", + table: "Notifications", + newName: "IsRead"); + + migrationBuilder.RenameColumn( + name: "jsonContent", + table: "Notifications", + newName: "Message"); + + migrationBuilder.AddColumn( + name: "Type", + table: "Notifications", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Type", + table: "Notifications"); + + migrationBuilder.RenameColumn( + name: "IsRead", + table: "Notifications", + newName: "isRead"); + + migrationBuilder.RenameColumn( + name: "Message", + table: "Notifications", + newName: "jsonContent"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index 0d3ad2e..bdfe5a3 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -18,6 +18,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -263,22 +266,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("BIT") .HasDefaultValue(false); + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + b.Property("ResourceId") .HasColumnType("int"); + b.Property("Type") + .HasColumnType("int"); + b.Property("UpdatedAt") .HasColumnType("DATETIME"); b.Property("UserId") .HasColumnType("int"); - b.Property("isRead") - .HasColumnType("bit"); - - b.Property("jsonContent") - .IsRequired() - .HasColumnType("NVARCHAR"); - b.HasKey("Id"); b.HasIndex("UserId"); diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index 799ba14..2807d59 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -9,10 +9,10 @@ public class Notification : ITrackable public int Id { get; set; } public int UserId { get; set; } public virtual ApplicationUser? User { get; set; } - public bool isRead { get; set; } + public bool IsRead { get; set; } public int ResourceId { get; set; } - public string jsonContent { get; set; } + public string Message { get; set; } public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs index afeda9b..0423d4f 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs @@ -10,10 +10,10 @@ public void Configure(EntityTypeBuilder builder) { builder.HasKey(n => n.Id); - builder.Property(n => n.jsonContent) + builder.Property(n => n.Message) .HasColumnType("NVARCHAR"); - builder.Property(n => n.isRead) + builder.Property(n => n.IsRead) .IsRequired(); From 3e53d296f4f734f3a77307c680a539de51c8c58c Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Mon, 25 Aug 2025 00:07:48 +0300 Subject: [PATCH 36/92] Enable Swagger --- AskFm/AskFm.API/AskFm.API.csproj | 1 + AskFm/AskFm.API/Program.cs | 9 ++++++++- AskFm/AskFm.API/Properties/launchSettings.json | 6 ++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index 6957d66..8fb3332 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -17,6 +17,7 @@ + diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index c07d4a8..8b14da4 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -50,7 +50,14 @@ public static void Main(string[] args) var app = builder.Build(); // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) app.MapOpenApi(); + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/openapi/v1.json", "api"); + }); + } app.UseHttpsRedirection(); diff --git a/AskFm/AskFm.API/Properties/launchSettings.json b/AskFm/AskFm.API/Properties/launchSettings.json index dc599be..75f6fa6 100644 --- a/AskFm/AskFm.API/Properties/launchSettings.json +++ b/AskFm/AskFm.API/Properties/launchSettings.json @@ -4,7 +4,8 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, + "launchUrl": "swagger", "applicationUrl": "http://localhost:5180", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -13,7 +14,8 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, + "launchUrl": "swagger", "applicationUrl": "https://localhost:7115;http://localhost:5180", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" From f85c186124c225095e56825a35e95f87a52cd7ba Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Mon, 25 Aug 2025 16:56:17 +0300 Subject: [PATCH 37/92] Feat: - Provide response server pattern - Implement Authentication using jwt and refresh token - Service Layer - Create 2 Services one for Authentication and the other for user operations - APIs - Auth Controller: and most of its endpoint not authorized as it's used for authentications process and handleing tokens lifes - User Congroller: authorized controller for user usuall operations - Completed APIs - register - login - Refresh Token - logout - get current user APIs TODO: - getuserbyid - EditUser - reset password - confirm email needed Refatoring: 1. Map DTOs in app layer not service layer 2. make new token in referesh process to take the same expire date as previous to prevent inite available token 3. enhancement the implementatin for logout api --- AskFm/AskFm.API/Controllers/AuthController.cs | 111 +++ AskFm/AskFm.API/Controllers/UserController.cs | 53 +- AskFm/AskFm.API/Program.cs | 39 +- AskFm/AskFm.BLL/AskFm.BLL.csproj | 4 + .../AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs | 11 + AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs | 3 +- .../AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs | 1 + AskFm/AskFm.BLL/Services/ServiceResult.cs | 6 +- .../UserIdentityService/AuthService.cs | 251 +++++- .../UserIdentityService/IAuthService.cs | 11 +- .../UserIdentityService/IUserService.cs | 18 +- .../UserIdentityService/JwtOptions.cs | 2 + .../UserIdentityService/UserService.cs | 54 +- ...824164944_AddRefreshTokenTable.Designer.cs | 794 +++++++++++++++++ .../20250824164944_AddRefreshTokenTable.cs | 44 + ...8_AddColumnToRefreshTokenTable.Designer.cs | 797 +++++++++++++++++ ...0824210328_AddColumnToRefreshTokenTable.cs | 30 + ...24832_FixIsActiveColumn_Follow.Designer.cs | 799 ++++++++++++++++++ ...20250824224832_FixIsActiveColumn_Follow.cs | 33 + .../Migrations/AppDbContextModelSnapshot.cs | 44 +- AskFm/AskFm.DAL/Models/ApplicationUser.cs | 5 +- AskFm/AskFm.DAL/Models/Follow.cs | 2 +- AskFm/AskFm.DAL/Models/RefreshToken.cs | 14 + .../ModelsConfigrations/FollowConfigration.cs | 2 + 24 files changed, 3089 insertions(+), 39 deletions(-) create mode 100644 AskFm/AskFm.API/Controllers/AuthController.cs create mode 100644 AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250824164944_AddRefreshTokenTable.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250824164944_AddRefreshTokenTable.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250824210328_AddColumnToRefreshTokenTable.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250824210328_AddColumnToRefreshTokenTable.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250824224832_FixIsActiveColumn_Follow.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250824224832_FixIsActiveColumn_Follow.cs create mode 100644 AskFm/AskFm.DAL/Models/RefreshToken.cs diff --git a/AskFm/AskFm.API/Controllers/AuthController.cs b/AskFm/AskFm.API/Controllers/AuthController.cs new file mode 100644 index 0000000..58b635d --- /dev/null +++ b/AskFm/AskFm.API/Controllers/AuthController.cs @@ -0,0 +1,111 @@ +using AskFm.BLL.DTO.UserDTOs; +using AskFm.BLL.Services; +using AskFm.BLL.Services.UserIdentityService; +using AskFm.DAL.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Sprache; + +namespace AskFm.API.Controllers; +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + private IAuthService _authService; + + public AuthController(IAuthService authService) + { + _authService = authService; + } + + [HttpPost] + [Route("register")] + public async Task RegisterUser(RegisterUserDTO registerUser) + { + if (registerUser == null) + { + return BadRequest(new List{"Invalid data"}); + } + ServiceResult result = await _authService.RegisterAsync(registerUser); + if (!result.success) + { + return BadRequest(result); + } + setRefreshToken(result.Data.RefreshToken.Token,result.Data.RefreshToken.ExpireOn); + return Ok(result); + } + + [HttpPost] + [Route("login")] + public async Task Login(LoginDTO login) + { + if (login == null) + { + return BadRequest(new List{"Invalid data"}); + } + ServiceResult result = await _authService.LoginAsync(login); + if (!result.success) + { + return BadRequest(result); + } + if (!string.IsNullOrEmpty(result.Data.Token)) + { + setRefreshToken(result.Data.RefreshToken.Token,result.Data.RefreshToken.ExpireOn); + } + return Ok(result); + } + + [HttpGet] + [Route("refresh-token/{id}")] + [Authorize(AuthenticationSchemes = "Bearer")] + public async Task RefreshToken(int id) + { + string refreshToken = Request.Cookies["refreshToken"]; + if (string.IsNullOrEmpty(refreshToken)) + { + return Unauthorized("Invalid Token"); + } + ServiceResult result = await _authService.RefreshTokenAsync(id,refreshToken); + + if (!result.success) + { + return BadRequest(result); + } + + return Ok(result); + + } + + [HttpPost("logout/{id}")] + [Authorize(AuthenticationSchemes = "Bearer")] + public async Task Logout(int id) + { + string refreshToken = Request.Cookies["refreshToken"]; + if (string.IsNullOrEmpty(refreshToken)) + { + return BadRequest("token Is required"); + } + + ServiceResult result = await _authService.RevokeRefreshTokenAsync(id,refreshToken); + + if (!result.success) + { + return BadRequest(result); + } + + + return Ok(result); + } + + private void setRefreshToken(string refreshToken,DateTime expires) + { + var cookieOption = new CookieOptions() + { + HttpOnly = true, + Expires = expires.ToLocalTime() + }; + Response.Cookies.Append("refreshToken", refreshToken, cookieOption); + } + +} \ No newline at end of file diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 8335b50..87a3a97 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -1,19 +1,62 @@ +using AskFm.BLL.DTO.UserDTOs; +using AskFm.BLL.Services.UserIdentityService; +using AskFm.DAL; +using AskFm.DAL.Interfaces; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace AskFm.API.Controllers; [ApiController] -[Route("[controller]")] +[Route("api/[controller]")] +[Authorize(AuthenticationSchemes = "Bearer")] public class UserController : ControllerBase { - /* - Register - login - logout + private IUnitOfWork _unitOfWork; + private IAuthService _authService; + public IUserService _userService; + + public UserController(IUnitOfWork unitOfWork, IAuthService authService, IUserService userService) + { + _unitOfWork = unitOfWork; + _authService = authService; + _userService = userService; + } + [HttpGet] + [Route("GetUsers")] + public async Task GetUsers() + { + var result = _unitOfWork.Users.GetAll().Select(u => new + { + name = u.Name, + email = u.Email, + username = u.UserName, + bio = u.Bio, + }).ToList(); + return Ok(result); + } + + [HttpGet] + [Route("current-user")] + public async Task GetCurrentUserAsync() + { + var result = await _userService.GetCurrentUserAsync(); + + if (!result.success) + { + return BadRequest(result.Errors); + } + return Ok(result.Data); + } + /* + GET Users only for now + getUserbyId EditUser DeleteUser FollowUser unfollowUser + reset password + confirm email Helper Function: getCurrentUserId */ diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 025c86d..bf1d8f9 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -35,47 +35,64 @@ public static void Main(string[] args) { throw new Exception("Connection string is null"); } - + builder.Services.AddHttpContextAccessor(); builder.Services.AddDbContext(options => options .UseLazyLoadingProxies() .UseSqlServer(ConnectionString)); - - - builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); + JwtOptions jwtOptions = new JwtOptions { Issuer = Environment.GetEnvironmentVariable("ISSUER"), Audience = Environment.GetEnvironmentVariable("AUDIENCE"), SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"), + AccessExpiration = int.Parse(Environment.GetEnvironmentVariable("TOKEN_EXP")), + AccessRefreshTokenExpiration = int.Parse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXP")), }; if (jwtOptions == null) { throw new Exception("jwtOptions is null"); } + + builder.Services.Configure(Options => + { + Options.Issuer = Environment.GetEnvironmentVariable("ISSUER"); + Options.Audience = Environment.GetEnvironmentVariable("AUDIENCE"); + Options.SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"); + Options.AccessExpiration = int.Parse(Environment.GetEnvironmentVariable("TOKEN_EXP")); + Options.AccessRefreshTokenExpiration = int.Parse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXP")); + }); - Console.WriteLine(jwtOptions.Issuer + " " + jwtOptions.Audience + " " + jwtOptions.SigningKey); - builder.Services.AddAuthentication() - .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, Options => + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = "Bearer"; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + + }) + .AddJwtBearer( Options => { Options.TokenValidationParameters = new TokenValidationParameters { + ValidateLifetime = true, ValidateIssuer = true, ValidIssuer = jwtOptions.Issuer, ValidateAudience = true, ValidAudience = jwtOptions.Audience, ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)) + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)), + ClockSkew = TimeSpan.FromMinutes(0) }; }); - + builder.Services.AddAuthorization(); builder.Services.AddIdentity>(options => { //password configuration @@ -116,9 +133,9 @@ public static void Main(string[] args) } app.UseHttpsRedirection(); - + app.UseAuthentication(); app.UseAuthorization(); - + app.MapControllers(); diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index ba85168..6977b2a 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs new file mode 100644 index 0000000..7948440 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs @@ -0,0 +1,11 @@ +using AskFm.DAL.Models; + +namespace AskFm.BLL.DTO.UserDTOs; + +public class AuthResponseDTO +{ + public bool IsAuthenticated { get; set; } + public string Token { get; set; } + public RefreshToken RefreshToken { get; set; } + public ReadUserDTO User { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs index 01913c2..6b8b600 100644 --- a/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs @@ -2,5 +2,6 @@ namespace AskFm.BLL.DTO.UserDTOs; public class LoginDTO { - + public String Email { get; set; } + public String Password { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs index 23a5bcf..5433972 100644 --- a/AskFm/AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs @@ -3,6 +3,7 @@ namespace AskFm.BLL.DTO.UserDTOs; public class RegisterUserDTO { public string Name { get; set; } + public string Username { get; set; } public string Email { get; set; } public string Bio { get; set; } public string AvatarPath { get; set; } diff --git a/AskFm/AskFm.BLL/Services/ServiceResult.cs b/AskFm/AskFm.BLL/Services/ServiceResult.cs index b8af2fa..74c8c58 100644 --- a/AskFm/AskFm.BLL/Services/ServiceResult.cs +++ b/AskFm/AskFm.BLL/Services/ServiceResult.cs @@ -6,7 +6,7 @@ public class ServiceResult public List? Errors { get; set; } public T? Data { get; set; } - public static ServiceResult Success(T data) + public static async Task> Success(T data) { return new ServiceResult { @@ -14,7 +14,7 @@ public static ServiceResult Success(T data) Data = data }; } - public static ServiceResult Success() + public static async Task> Success() { return new ServiceResult { @@ -22,7 +22,7 @@ public static ServiceResult Success() }; } - public static ServiceResult Failure(List errors) + public static async Task> Failure(List errors) { return new ServiceResult { diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs index 3770009..1bbcc0e 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs @@ -1,26 +1,259 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; using AskFm.BLL.DTO.UserDTOs; +using AskFm.DAL; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using Azure; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Server.HttpSys; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; namespace AskFm.BLL.Services.UserIdentityService; public class AuthService : IAuthService { - public Task> LoginAsync(LoginDTO request) + private IUnitOfWork _unitOfWork; + private readonly UserManager _userManager; + private readonly JwtOptions _jwtOptions; + + public AuthService(IUnitOfWork unitOfWork, UserManager userManager, IOptions jwtOptions) { - throw new NotImplementedException(); + _unitOfWork = unitOfWork; + _userManager = userManager; + _jwtOptions = jwtOptions.Value; + } + + public async Task> LoginAsync(LoginDTO request) + { + if (request == null) + { + var erros = new List + { + "Invalid Email or Password." + }; + return await ServiceResult.Failure(erros); + } + + var getUser = await _userManager.FindByEmailAsync(request.Email); + + if (getUser == null) + { + var erros = new List + { + "Invalid Email or Password." + }; + return await ServiceResult.Failure(erros); + } + + var passwordValid = await _userManager.CheckPasswordAsync(getUser, request.Password); + if (!passwordValid) + { + var errors = new List { "Invalid Email or Password." }; + return await ServiceResult.Failure(errors); + } + + var response = await GetAuthToken(getUser); + return response; + } + - public Task> RegisterAsync(RegisterUserDTO request) + public async Task> RegisterAsync(RegisterUserDTO request) { - throw new NotImplementedException(); + if (request == null) + { + var errors = new List{ "Invalid Request Data" }; + return await ServiceResult.Failure(errors); + } + var oldUser = _userManager.FindByEmailAsync(request.Email).Result; + if (oldUser != null) + { + var errors = new List{ "Email already exist" }; + return await ServiceResult.Failure(errors); + } + + var newUser = new ApplicationUser() + { + Name = request.Name, + Email = request.Email, + UserName = request.Username, + Bio = request.Bio, + AvatarPath = request.AvatarPath, + LastSeen = DateTime.UtcNow + }; + var createRsult = await _userManager.CreateAsync(newUser,request.Passwrod); + + + if (createRsult.Succeeded == false) + { + + var errors = createRsult.Errors.Select(e => e.Description).ToList(); + return await ServiceResult.Failure(errors); + + + } + + var response = await GetAuthToken(newUser); + return response; } - public Task> RevokeRefreshTokenAsync(string refreshToken) + public async Task> RefreshTokenAsync(int id, string refreshToken) { - throw new NotImplementedException(); + if (refreshToken == null) + { + var errors = new List { "Invalid Token." }; + return await ServiceResult.Failure(errors); + } + + var user = _unitOfWork.Users.GetById(id); + if (user == null) + { + var errors = new List { "Invalid User." }; + return await ServiceResult.Failure(errors); + } + if (!user.RefreshTokens.Any(r => r.Token == refreshToken)) + { + var errors = new List { "Invalid Token." }; + return await ServiceResult.Failure(errors); + } + + var oldRefreshToken = user.RefreshTokens.Single(t => t.Token == refreshToken); + if (!oldRefreshToken.IsActive) + { + var errors = new List { "InActive Token." }; + return await ServiceResult.Failure(errors); + } + + oldRefreshToken.RevokedOn = DateTime.UtcNow; + + return await GetAuthToken(user); + } - public Task> RevokeAllRefreshTokensAsync(int userId) + + public async Task> RevokeRefreshTokenAsync(int id, string refreshToken) { - throw new NotImplementedException(); + if (string.IsNullOrEmpty(refreshToken)) + { + var errors = new List { "Invalid Token." }; + return await ServiceResult.Failure(errors); + } + + var user = _unitOfWork.Users.GetById(id); + if (user == null) + { + var errors = new List { "Invalid User." }; + return await ServiceResult.Failure(errors); + } + + if (!user.RefreshTokens.Any(r => r.Token == refreshToken)) + { + var errors = new List { "Invalid Token." }; + return await ServiceResult.Failure(errors); + } + + var oldRefreshToken = user.RefreshTokens.Single(t => t.Token == refreshToken); + + if (!oldRefreshToken.IsActive) + { + var errors = new List { "InActive Token." }; + return await ServiceResult.Failure(errors); + } + + oldRefreshToken.RevokedOn = DateTime.UtcNow; + + await _userManager.UpdateAsync(user); + + return await ServiceResult.Success(true); + } -} \ No newline at end of file + + public void Logout() + { + + + } + + + private async Task> GetAuthToken(ApplicationUser user) + { + var token = await GenerateJwtToken(user); + if (string.IsNullOrEmpty(token)) + { + var errors = new List { "Invalid Data" }; + return await ServiceResult.Failure(errors); + } + + RefreshToken refreshToken = null; + if (user.RefreshTokens !=null && user.RefreshTokens.Any(r => r.IsActive)) + { + refreshToken = user.RefreshTokens.FirstOrDefault(r => r.IsActive); + } + else + { + refreshToken = await generateRefreshToken(); + user.RefreshTokens ??= new List(); + user.RefreshTokens.Add(refreshToken); + } + + user.LastSeen = DateTime.UtcNow; + await _userManager.UpdateAsync(user); + + return await ServiceResult.Success(new AuthResponseDTO() + { + Token = token, + RefreshToken = refreshToken, + IsAuthenticated = true, + User = new ReadUserDTO + { + Name = user.Name, + Email = user.Email, + LastSeen = user.LastSeen, + Bio = user.Bio, + AvatarPath = user.AvatarPath, + followerCount = user.FollowersCount + } + }); + } + private async Task generateRefreshToken() + { + var randomNumber = new byte[32]; + + using var generator = new RNGCryptoServiceProvider(); + + generator.GetBytes(randomNumber); + + return new RefreshToken + { + Token = Convert.ToBase64String(randomNumber), + ExpireOn = DateTime.UtcNow.AddDays(10), + CreatedOn = DateTime.UtcNow + }; + + } + private Task GenerateJwtToken(ApplicationUser appUser) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor(){ + Issuer = _jwtOptions.Issuer, + Audience = _jwtOptions.Audience, + Expires = DateTime.UtcNow.AddMinutes(_jwtOptions.AccessExpiration), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SigningKey)), SecurityAlgorithms.HmacSha256), + Subject = new ClaimsIdentity(new Claim[] + { + new(ClaimTypes.Name, appUser.Name), + new(ClaimTypes.Email, appUser.Email), + new("UserId", appUser.Id.ToString()) + }) + }; + var securityToken = tokenHandler.CreateToken(tokenDescriptor); + var accessToken = tokenHandler.WriteToken(securityToken); + return Task.FromResult(accessToken); + } +} + diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs index 855b766..249eb83 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs @@ -1,12 +1,13 @@ using AskFm.BLL.DTO.UserDTOs; +using AskFm.DAL.Models; namespace AskFm.BLL.Services.UserIdentityService; public interface IAuthService { - public Task> LoginAsync(LoginDTO request); - public Task> RegisterAsync(RegisterUserDTO request); - // Task> RefreshTokenAsync(string refreshToken); - public Task> RevokeRefreshTokenAsync(string refreshToken); - public Task> RevokeAllRefreshTokensAsync(int userId); + public Task> LoginAsync(LoginDTO request); + public Task> RegisterAsync(RegisterUserDTO request); + Task> RefreshTokenAsync(int id, string refreshToken); + public Task> RevokeRefreshTokenAsync(int id, string refreshToken); + public void Logout(); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs index 790adf4..51a9e21 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -9,5 +9,21 @@ public interface IUserService Task> FollowUserAsync(int followerId, int targetUserId); Task> UnfollowUserAsync(int followerId, int targetUserId); Task UpdateLastSeenAsync(int userId, DateTime lastSeen); - Task GetUserByIdAsync(int userId); + Task> GetUserByIdAsync(int userId); + Task> GetCurrentUserAsync(); + Task> ResetPassword(string newPassword); + Task> ConfirmEmail(); + + + /* +GET Users only for now + getUserbyId + EditUser + DeleteUser + FollowUser + unfollowUser + reset password + confirm email + Helper Function: getCurrentUserId +*/ } diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs index 57c15fa..6d5e48f 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs @@ -5,4 +5,6 @@ public class JwtOptions public string Issuer { get; set; } public string Audience { get; set; } public string SigningKey { get; set; } + public int AccessExpiration { get; set; } // Minutes + public int AccessRefreshTokenExpiration { get; set; } // days } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index fc7823c..9169703 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -1,9 +1,27 @@ +using System.Security.Claims; using AskFm.BLL.DTO.UserDTOs; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; namespace AskFm.BLL.Services.UserIdentityService; public class UserService : IUserService { + private IUnitOfWork _unitOfWork; + private readonly UserManager _userManager; + private readonly IHttpContextAccessor _httpContextAccessor; + + + public UserService(IUnitOfWork unitOfWork, UserManager userManager, IHttpContextAccessor httpContextAccessor) + { + _unitOfWork = unitOfWork; + _userManager = userManager; + _httpContextAccessor = httpContextAccessor; + } + + public Task> UpdateUserAsync(int userId, UpdateUserDTO updatedUser) { throw new NotImplementedException(); @@ -29,7 +47,41 @@ public Task UpdateLastSeenAsync(int userId, DateTime lastSeen) throw new NotImplementedException(); } - public Task GetUserByIdAsync(int userId) + public Task> GetUserByIdAsync(int userId) + { + throw new NotImplementedException(); + } + + public async Task> GetCurrentUserAsync() + { + string email = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.Email).Value; + if (string.IsNullOrEmpty(email)) + { + var errors = new List() + { + "Can't Access Current user" + }; + return await ServiceResult.Failure(errors); + } + var currentAppUser = await _userManager.FindByEmailAsync(email); + + return await ServiceResult.Success(new ReadUserDTO() + { + Name = currentAppUser.Name, + Email = currentAppUser.Email, + LastSeen = currentAppUser.LastSeen, + Bio = currentAppUser.Bio, + AvatarPath = currentAppUser.AvatarPath, + followerCount = currentAppUser.FollowersCount + }); + } + + public Task> ResetPassword(string newPassword) + { + throw new NotImplementedException(); + } + + public Task> ConfirmEmail() { throw new NotImplementedException(); } diff --git a/AskFm/AskFm.DAL/Migrations/20250824164944_AddRefreshTokenTable.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250824164944_AddRefreshTokenTable.Designer.cs new file mode 100644 index 0000000..aab3af4 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250824164944_AddRefreshTokenTable.Designer.cs @@ -0,0 +1,794 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250824164944_AddRefreshTokenTable")] + partial class AddRefreshTokenTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("isRead") + .HasColumnType("bit"); + + b.Property("jsonContent") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.OwnsMany("AskFm.DAL.Models.RefreshToken", "RefreshTokens", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("ExpireOn") + .HasColumnType("datetime2"); + + b1.Property("RevokedOn") + .HasColumnType("datetime2"); + + b1.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId", "Id"); + + b1.ToTable("RefreshToken"); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250824164944_AddRefreshTokenTable.cs b/AskFm/AskFm.DAL/Migrations/20250824164944_AddRefreshTokenTable.cs new file mode 100644 index 0000000..bbfba81 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250824164944_AddRefreshTokenTable.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class AddRefreshTokenTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RefreshToken", + columns: table => new + { + ApplicationUserId = table.Column(type: "int", nullable: false), + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Token = table.Column(type: "nvarchar(max)", nullable: false), + ExpireOn = table.Column(type: "datetime2", nullable: false), + RevokedOn = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshToken", x => new { x.ApplicationUserId, x.Id }); + table.ForeignKey( + name: "FK_RefreshToken_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RefreshToken"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250824210328_AddColumnToRefreshTokenTable.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250824210328_AddColumnToRefreshTokenTable.Designer.cs new file mode 100644 index 0000000..d37c369 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250824210328_AddColumnToRefreshTokenTable.Designer.cs @@ -0,0 +1,797 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250824210328_AddColumnToRefreshTokenTable")] + partial class AddColumnToRefreshTokenTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("isRead") + .HasColumnType("bit"); + + b.Property("jsonContent") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.OwnsMany("AskFm.DAL.Models.RefreshToken", "RefreshTokens", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("CreatedOn") + .HasColumnType("datetime2"); + + b1.Property("ExpireOn") + .HasColumnType("datetime2"); + + b1.Property("RevokedOn") + .HasColumnType("datetime2"); + + b1.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId", "Id"); + + b1.ToTable("RefreshToken"); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250824210328_AddColumnToRefreshTokenTable.cs b/AskFm/AskFm.DAL/Migrations/20250824210328_AddColumnToRefreshTokenTable.cs new file mode 100644 index 0000000..56bfefa --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250824210328_AddColumnToRefreshTokenTable.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class AddColumnToRefreshTokenTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CreatedOn", + table: "RefreshToken", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatedOn", + table: "RefreshToken"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250824224832_FixIsActiveColumn_Follow.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250824224832_FixIsActiveColumn_Follow.Designer.cs new file mode 100644 index 0000000..c5f89ee --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250824224832_FixIsActiveColumn_Follow.Designer.cs @@ -0,0 +1,799 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250824224832_FixIsActiveColumn_Follow")] + partial class FixIsActiveColumn_Follow + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("isRead") + .HasColumnType("bit"); + + b.Property("jsonContent") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.OwnsMany("AskFm.DAL.Models.RefreshToken", "RefreshTokens", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("CreatedOn") + .HasColumnType("datetime2"); + + b1.Property("ExpireOn") + .HasColumnType("datetime2"); + + b1.Property("RevokedOn") + .HasColumnType("datetime2"); + + b1.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId", "Id"); + + b1.ToTable("RefreshToken"); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250824224832_FixIsActiveColumn_Follow.cs b/AskFm/AskFm.DAL/Migrations/20250824224832_FixIsActiveColumn_Follow.cs new file mode 100644 index 0000000..98d3232 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250824224832_FixIsActiveColumn_Follow.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class FixIsActiveColumn_Follow : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsActive", + table: "Follows", + type: "BIT", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsActive", + table: "Follows", + type: "bit", + nullable: false, + defaultValue: true + ); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index 0d3ad2e..796abaa 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -18,6 +18,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -227,7 +230,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("DATETIME"); b.Property("IsActive") - .HasColumnType("bit"); + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(true); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -531,6 +536,43 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.OwnsMany("AskFm.DAL.Models.RefreshToken", "RefreshTokens", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("CreatedOn") + .HasColumnType("datetime2"); + + b1.Property("ExpireOn") + .HasColumnType("datetime2"); + + b1.Property("RevokedOn") + .HasColumnType("datetime2"); + + b1.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId", "Id"); + + b1.ToTable("RefreshToken"); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("RefreshTokens"); + }); + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => { b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") diff --git a/AskFm/AskFm.DAL/Models/ApplicationUser.cs b/AskFm/AskFm.DAL/Models/ApplicationUser.cs index f45c39f..08f0869 100644 --- a/AskFm/AskFm.DAL/Models/ApplicationUser.cs +++ b/AskFm/AskFm.DAL/Models/ApplicationUser.cs @@ -6,7 +6,6 @@ namespace AskFm.DAL.Models; public class ApplicationUser : IdentityUser, ITrackable { public string Name { get; set; } - public string Email { get; set; } public string Bio { get; set; } public string AvatarPath { get; set; } @@ -29,4 +28,8 @@ public class ApplicationUser : IdentityUser, ITrackable public DateTime DeletedAt { get; set; } public DateTime UpdatedAt { get; set; } public DateTime CreatedAt { get; set; } + + + // tokens + public virtual ICollection? RefreshTokens { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Follow.cs b/AskFm/AskFm.DAL/Models/Follow.cs index 1c41965..af28686 100644 --- a/AskFm/AskFm.DAL/Models/Follow.cs +++ b/AskFm/AskFm.DAL/Models/Follow.cs @@ -11,7 +11,7 @@ public class Follow : ITrackable public virtual ApplicationUser? Followed { get; set; } // is this follow available - public bool IsActive { get; set; } = true; + public bool IsActive { get; set; } public bool IsDeleted { get; set; } diff --git a/AskFm/AskFm.DAL/Models/RefreshToken.cs b/AskFm/AskFm.DAL/Models/RefreshToken.cs new file mode 100644 index 0000000..d1bbc9f --- /dev/null +++ b/AskFm/AskFm.DAL/Models/RefreshToken.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace AskFm.DAL.Models; +[Owned] +public class RefreshToken +{ + public string Token { get; set; } + public DateTime ExpireOn { get; set; } + public bool IsExpired => DateTime.Now >= ExpireOn; + public DateTime? RevokedOn { get; set; } + public bool IsActive => RevokedOn == null && !IsExpired; + + public DateTime CreatedOn { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/FollowConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/FollowConfigration.cs index bae42e9..8b868b2 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/FollowConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/FollowConfigration.cs @@ -20,5 +20,7 @@ public void Configure(EntityTypeBuilder builder) .WithMany(u => u.Followers) .HasForeignKey(f => f.FollowedId) .OnDelete(DeleteBehavior.NoAction); + + builder.Property(f => f.IsActive).HasColumnType("BIT").HasDefaultValue(1); } } \ No newline at end of file From 40c3c7e50adc060ed3d01f7e3da196a81379ee90 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sun, 24 Aug 2025 23:22:57 +0300 Subject: [PATCH 38/92] Add: - Added CommentController (Adding 3 endpoints mentioned on API Design issue) - Added ICommentService, CommentService (With one function to get the comment by id) - Added ICommentLikeService and CommentLikeService to handle the core logic function for the Comment likes. - Added CommentLikeServiceTest to write Unit tests for the CommentlikeService Class. # Conflicts: # AskFm/AskFm.API/Program.cs # AskFm/AskFm.BLL/AskFm.BLL.csproj --- .../Controllers/CommentController.cs | 124 +++++++++ AskFm/AskFm.BLL/DTO/CommentLikeDto.cs | 9 + AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs | 8 + .../AskFm.BLL/Services/CommentLikeService.cs | 147 +++++++++++ AskFm/AskFm.BLL/Services/CommentService.cs | 21 ++ .../AskFm.BLL/Services/ICommentLikeService.cs | 10 + AskFm/AskFm.BLL/Services/ICommentService.cs | 8 + AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs | 2 +- AskFm/AskFm.DAL/UnitOfWork.cs | 2 +- AskFm/Tests/CommentLikeServiceTest.cs | 244 ++++++++++++++++++ AskFm/Tests/Tests.csproj | 5 + 11 files changed, 578 insertions(+), 2 deletions(-) create mode 100644 AskFm/AskFm.API/Controllers/CommentController.cs create mode 100644 AskFm/AskFm.BLL/DTO/CommentLikeDto.cs create mode 100644 AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs create mode 100644 AskFm/AskFm.BLL/Services/CommentLikeService.cs create mode 100644 AskFm/AskFm.BLL/Services/CommentService.cs create mode 100644 AskFm/AskFm.BLL/Services/ICommentLikeService.cs create mode 100644 AskFm/AskFm.BLL/Services/ICommentService.cs create mode 100644 AskFm/Tests/CommentLikeServiceTest.cs diff --git a/AskFm/AskFm.API/Controllers/CommentController.cs b/AskFm/AskFm.API/Controllers/CommentController.cs new file mode 100644 index 0000000..4ce0768 --- /dev/null +++ b/AskFm/AskFm.API/Controllers/CommentController.cs @@ -0,0 +1,124 @@ +using AskFm.BLL.DTO; +using AskFm.BLL.Services; +using AskFm.DAL.Interfaces; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; + +namespace AskFm.API.Controllers; + +[ApiController] +[Route("[controller]")] +public class CommentController : ControllerBase +{ + + private readonly ICommentLikeService _commentLikeService; + private readonly ICommentService _commentService; + private readonly ILogger _logger; + + + public CommentController( + ICommentLikeService commentLikeService, + ICommentService commentService, + ILogger logger) + { + _commentLikeService = commentLikeService; + _logger = logger; + _commentService = commentService; + } + + + + + + // GET api/comment/{id}/likes -> get all the likes for a Comment with id = id + [HttpGet("{id}/likes")] + public async Task GetAllLikes(int id) + { + try + { + var likes = await _commentLikeService.GetLikesForCommentAsync(id); + return Ok(likes); + } + catch (ArgumentException ex) + { + _logger.LogWarning(ex, "Comment not found with id: {CommentId}", id); + return NotFound(new { message = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving likes for comment id: {CommentId}", id); + return StatusCode(500, new { message = "An error occurred while retrieving likes" }); + } + } + + + + + // POST api/comment/{id}/likes -> add a like for a Comment with id = id + [HttpPost("{id}/likes")] + public async Task AddLike(int id, [FromBody] CreateCommentLikeDto likeDto) + { + try + { + var userId = likeDto.UserId; + + var createdLike = await _commentLikeService.AddLikeAsync(id, userId); + + return CreatedAtAction( + nameof(GetAllLikes), + new { id = id }, + createdLike); + } + catch (ArgumentException ex) + { + _logger.LogWarning(ex, "Invalid request to add like to comment id: {CommentId}", id); + return BadRequest(new { message = ex.Message }); + } + catch (InvalidOperationException ex) + { + _logger.LogWarning(ex, "Cannot add like to comment id: {CommentId}", id); + return Conflict(new { message = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding like to comment id: {CommentId}", id); + return StatusCode(500, new { message = "An error occurred while adding like" }); + } + } + + + + + [HttpDelete("{id}/likes")] + public async Task DeleteLike(int id) + { + try + { + var comment = _commentService.GetComment(id); + if (comment == null) + throw new ArgumentException($"Comment with id {id} not found"); + + var userId = comment.UserId; + + await _commentLikeService.DeleteLikeAsync(id, userId.Value); + + return NoContent(); + } + catch (ArgumentException ex) + { + _logger.LogWarning(ex, "Like not found for comment id: {CommentId} and user", id); + return NotFound(new { message = ex.Message }); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogWarning(ex, "Unauthorized delete attempt for comment id: {CommentId}", id); + return Forbid(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting like from comment id: {CommentId}", id); + return StatusCode(500, new { message = "An error occurred while deleting like" }); + } + } + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CommentLikeDto.cs b/AskFm/AskFm.BLL/DTO/CommentLikeDto.cs new file mode 100644 index 0000000..264b8bf --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CommentLikeDto.cs @@ -0,0 +1,9 @@ +namespace AskFm.BLL.DTO; + +public class CommentLikeDto +{ + public int CommentId { get; set; } + public int UserId { get; set; } + public string UserName { get; set; } + public DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs b/AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs new file mode 100644 index 0000000..3fdc72d --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs @@ -0,0 +1,8 @@ +namespace AskFm.BLL.DTO; + +public class CreateCommentLikeDto +{ + public int CommentId; + public int UserId; + public DateTime CreatedAt; +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs new file mode 100644 index 0000000..b63d58c --- /dev/null +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -0,0 +1,147 @@ +using AskFm.BLL.DTO; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using AutoMapper; +using Microsoft.Extensions.Logging; + +namespace AskFm.BLL.Services; + +public class CommentLikeService : ICommentLikeService +{ + + private IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly IMapper _mapper; + public CommentLikeService(IUnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + + + public async Task> GetLikesForCommentAsync(int commentId) + { + try + { + _logger.LogInformation("Retrieving likes for comment id: {CommentId}", commentId); + + var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); + if (comment == null) + { + throw new ArgumentException($"Comment with id {commentId} not found"); + } + + var likes = await _unitOfWork.CommentLikes.FindAllAsync( + predicate: cl => cl.CommentId == commentId && !cl.IsDeleted + ); + + var likeDtos = likes.Select(like => new CommentLikeDto + { + CommentId = like.CommentId, + UserId = like.UserId, + CreatedAt = like.CreatedAt, + UserName = like.User?.UserName + }); + + return likeDtos; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving likes for comment id: {CommentId}", commentId); + throw; + } + } + + public async Task AddLikeAsync(int commentId, int userId) + { + try + { + _logger.LogInformation("Adding like for comment id: {CommentId} by user id: {UserId}", + commentId, userId); + + var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); + + if (comment == null) + { + throw new ArgumentException($"Comment with id {commentId} not found"); + } + + var existingLike = await _unitOfWork.CommentLikes.FindAsync( + cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted + ); + + if (existingLike != null) + { + throw new InvalidOperationException("User has already liked this comment"); + } + + var newLike = new CommentLike + { + CommentId = commentId, + UserId = userId, + }; + + await _unitOfWork.CommentLikes.AddAsync(newLike); + + comment.LikeCount++; + _unitOfWork.Comments.Update(comment); + + await _unitOfWork.SaveAsync(); + + _logger.LogInformation("Like added successfully for comment id: {CommentId}", commentId); + + + var user = await _unitOfWork.Users.GetByIdAsync(userId); + string userName = user.UserName; + + + return new CommentLikeDto + { + CommentId = newLike.CommentId, + UserId = newLike.UserId, + UserName = userName, + CreatedAt = newLike.CreatedAt + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding like for comment id: {CommentId} by user id: {UserId}", + commentId, userId); + throw; + } + + } + + + public async Task DeleteLikeAsync(int commentId, int userId) + { + try + { + _logger.LogInformation("Deleting the comment like from user {userid} on comment id {commentId}", userId, commentId); + var commentLike = await _unitOfWork.CommentLikes.FindAsync(cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted); + + var comment = _unitOfWork.Comments.GetByIdAsync(commentId); + + Console.WriteLine(comment.Result.Content); + + // if the user didn't like this comment before + if(commentLike == null) + throw new ArgumentException($"User didn't like this comment"); + + await _unitOfWork.CommentLikes.RemoveAsync(commentLike); + + comment.Result.IsDeleted = true; + comment.Result.CommentLikes.Remove(commentLike); + comment.Result.LikeCount--; + + await _unitOfWork.SaveAsync(); + + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/CommentService.cs b/AskFm/AskFm.BLL/Services/CommentService.cs new file mode 100644 index 0000000..8cec928 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/CommentService.cs @@ -0,0 +1,21 @@ +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; + +namespace AskFm.BLL.Services; + +public class CommentService : ICommentService +{ + private IUnitOfWork _unitOfWork; + public CommentService(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public Comment GetComment(int commentId) + { + var comment = _unitOfWork.Comments.GetById(commentId); + return comment; + } + + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ICommentLikeService.cs b/AskFm/AskFm.BLL/Services/ICommentLikeService.cs new file mode 100644 index 0000000..712a4a4 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ICommentLikeService.cs @@ -0,0 +1,10 @@ +using AskFm.BLL.DTO; + +namespace AskFm.BLL.Services; + +public interface ICommentLikeService +{ + Task> GetLikesForCommentAsync(int commentId); + Task AddLikeAsync(int commentId, int userId); + Task DeleteLikeAsync(int commentId, int userId); +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ICommentService.cs b/AskFm/AskFm.BLL/Services/ICommentService.cs new file mode 100644 index 0000000..1a3e213 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ICommentService.cs @@ -0,0 +1,8 @@ +using AskFm.DAL.Models; + +namespace AskFm.BLL.Services; + +public interface ICommentService +{ + Comment GetComment(int commentId); +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs index 108f184..c4fd6ec 100644 --- a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs +++ b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs @@ -1,5 +1,5 @@ using AskFm.DAL.Models; -using Thread = System.Threading.Thread; +using Thread = AskFm.DAL.Models.Thread; namespace AskFm.DAL.Interfaces; diff --git a/AskFm/AskFm.DAL/UnitOfWork.cs b/AskFm/AskFm.DAL/UnitOfWork.cs index 573f3ae..a31c2f2 100644 --- a/AskFm/AskFm.DAL/UnitOfWork.cs +++ b/AskFm/AskFm.DAL/UnitOfWork.cs @@ -1,7 +1,7 @@ using AskFm.DAL.Interfaces; using AskFm.DAL.Models; using AskFm.DAL.Repositories; -using Thread = System.Threading.Thread; +using Thread = AskFm.DAL.Models.Thread; namespace AskFm.DAL; diff --git a/AskFm/Tests/CommentLikeServiceTest.cs b/AskFm/Tests/CommentLikeServiceTest.cs new file mode 100644 index 0000000..fec7191 --- /dev/null +++ b/AskFm/Tests/CommentLikeServiceTest.cs @@ -0,0 +1,244 @@ +using AskFm.BLL.Services; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using AutoMapper; +using Microsoft.Extensions.Logging; +using Moq; +using System.Linq.Expressions; +using AskFm.BLL.DTO; + + +namespace Tests; + +public class CommentLikeServiceTest +{ + private readonly Mock _mockUnitOfWork; + private readonly Mock> _loggerMock; + private readonly Mock> _mockCommentRepository; + private readonly Mock> _mockUserRepository; + private readonly Mock> _mockCommentLikeRepository; + private readonly CommentLikeService _commentLikeService; + + public CommentLikeServiceTest() + { + _mockUnitOfWork = new Mock(); + _mockCommentRepository = new Mock>(); + _mockCommentLikeRepository = new Mock>(); + _mockUserRepository = new Mock>(); + _loggerMock = new Mock>(); + _mockUnitOfWork.Setup(uow => uow.Comments).Returns(_mockCommentRepository.Object); + _mockUnitOfWork.Setup(uow => uow.CommentLikes).Returns(_mockCommentLikeRepository.Object); + _mockUnitOfWork.Setup(uow => uow.Users).Returns(_mockUserRepository.Object); + + + _commentLikeService = new CommentLikeService(_mockUnitOfWork.Object, _loggerMock.Object); + } + + [Fact] + public async Task AddLike_WhenCommentIsNotDeleted_AddingNewLikeOnTheComment() + { + // Arrange + var commentId = 1; + var userId = 5; + + var comment = new Comment + { + Id = commentId, + IsDeleted = false, + Content = "Test comment" + }; + + var user = new ApplicationUser() + { + Id = userId, + Comments = new List { comment } + }; + + + _mockCommentRepository.Setup(repo => repo.GetByIdAsync(commentId)) + .ReturnsAsync(comment); + + _mockUserRepository.Setup(repo => repo.GetByIdAsync(userId)) + .ReturnsAsync(user); + + // Act + await _commentLikeService.AddLikeAsync(commentId, userId); + + // Assert + _mockCommentLikeRepository.Verify(repo => repo.AddAsync(It.Is(cl => + cl.CommentId == commentId && cl.UserId == userId)), Times.Once); + + _mockUnitOfWork.Verify(uow => uow.SaveAsync(), Times.Once); + } + + [Fact] + public async void AddLike_WhenCommentNotFoundOrDeleted_ThrowException() + { + // Arrange + var commentId = 1000; + var userId = 5; + _mockCommentRepository.Setup(repo => repo.GetById(commentId)) + .Returns((Comment)null); + + // Assert + await Assert.ThrowsAsync(() => _commentLikeService.AddLikeAsync(commentId, userId)); + + _mockCommentLikeRepository.Verify(repo => repo.Add(It.IsAny()), Times.Never); + _mockUnitOfWork.Verify(uow => uow.Save(), Times.Never); + + } + + + [Fact] + public void DeleteLike_WhenDeleteByAuthor_CommentIsDeleted() + { + var commentId = 1; + var userId = 5; + + var commentLike = new CommentLike { CommentId = commentId, UserId = userId }; + + _mockCommentLikeRepository.Setup(repo => + repo.FindAsync( + It.Is>>(expr => + ExpressionMatches(expr, userId, commentId)), + It.IsAny() + )) + .Returns(Task.FromResult(commentLike)); + + var likeCount = 1; + + ICollection commentLikes = new List(); + commentLikes.Add(commentLike); + + var comment = new Comment() + { + Id = commentId, + Content = "Test comment", + IsDeleted = false, + CommentLikes = commentLikes, + LikeCount = likeCount + }; + + _mockCommentRepository.Setup(repo => repo.GetByIdAsync(commentId)) + .Returns(Task.FromResult(comment)); + + + _mockCommentLikeRepository.Setup(repo => repo.RemoveAsync(commentLike)) + .Callback(() => commentLike.IsDeleted = true); + + + // Act + _commentLikeService.DeleteLikeAsync(commentId, userId); + + + + // Assert + + Assert.Empty(comment.CommentLikes); + Assert.True(commentLike.IsDeleted); + Assert.Equal(comment.LikeCount, likeCount - 1); + _mockCommentLikeRepository.Verify(repo => repo.RemoveAsync(commentLike), Times.Once); + _mockUnitOfWork.Verify(uow => uow.SaveAsync(), Times.Once); + + } + + + [Fact] + public async void GetLikesForComment_WhenCommentLikesIsNotEmpty_ReturnsCommentLikesList() + { + var commentId = 1; + var userId = 4; + + + ICollection commentLikes = new List() + { + { new CommentLike { CommentId = commentId, UserId = 4 }}, + { new CommentLike { CommentId = commentId, UserId = 5 } }, + { new CommentLike { CommentId = commentId, UserId = 6 } }, + { new CommentLike { CommentId = commentId, UserId = 8 } }, + { new CommentLike { CommentId = commentId, UserId = 9 } }, + }; + + + + var comment = new Comment() + { + Id = commentId, + Content = "Test comment", + IsDeleted = false, + CommentLikes = commentLikes + }; + + var expectedDtos = new List + { + new CommentLikeDto {CommentId = commentId, UserId = 4 }, + new CommentLikeDto {CommentId = commentId, UserId = 5 }, + new CommentLikeDto {CommentId = commentId, UserId = 6 }, + new CommentLikeDto {CommentId = commentId, UserId = 8 }, + new CommentLikeDto {CommentId = commentId, UserId = 9 } + }; + + _mockCommentRepository.Setup(repo => repo.GetByIdAsync(commentId)) + .ReturnsAsync(comment); + + _mockCommentLikeRepository.Setup(repo => + repo.FindAllAsync(It.IsAny>>(), It.IsAny())) + .ReturnsAsync(commentLikes); + + // Act + var result = await _commentLikeService.GetLikesForCommentAsync(commentId); + + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Count()); + + Assert.Equivalent(expectedDtos, result); + + } + + + [Fact] + public async void GetLikesForComment_WhenCommentLikesIsEmpty_ReturnsEmptyList() + { + // Arrange + var commentId = 1; + var userId = 4; + + ICollection commentLikes = new List(); + + var comment = new Comment() + { + Id = commentId, + Content = "Test comment", + IsDeleted = false, + CommentLikes = commentLikes + }; + + _mockCommentRepository.Setup(repo => repo.GetByIdAsync(commentId)) + .ReturnsAsync(comment); + + _mockCommentLikeRepository.Setup(repo => + repo.FindAllAsync(It.IsAny>>(), It.IsAny())) + .ReturnsAsync(commentLikes); + + // Act + var result = await _commentLikeService.GetLikesForCommentAsync(commentId); + + + // Assert + Assert.Empty(result); + + } + + + // helper + private bool ExpressionMatches(Expression> expr, int userId, int commentId) + { + var testItem = new CommentLike { UserId = userId, CommentId = commentId }; + var compiled = expr.Compile(); + return compiled(testItem); + } + + +} \ No newline at end of file diff --git a/AskFm/Tests/Tests.csproj b/AskFm/Tests/Tests.csproj index 95d873d..2e91a7c 100644 --- a/AskFm/Tests/Tests.csproj +++ b/AskFm/Tests/Tests.csproj @@ -10,6 +10,7 @@ + @@ -18,4 +19,8 @@ + + + + From d8cfb1b522228cfa073259d36b2aded2716ffd43 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 25 Aug 2025 05:45:35 +0300 Subject: [PATCH 39/92] FIX: Fixing a userId bug on the Delete Like endpoint --- AskFm/AskFm.API/Controllers/CommentController.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/CommentController.cs b/AskFm/AskFm.API/Controllers/CommentController.cs index 4ce0768..a6a0fef 100644 --- a/AskFm/AskFm.API/Controllers/CommentController.cs +++ b/AskFm/AskFm.API/Controllers/CommentController.cs @@ -90,7 +90,7 @@ public async Task AddLike(int id, [FromBody] CreateCommentLikeDto [HttpDelete("{id}/likes")] - public async Task DeleteLike(int id) + public async Task DeleteLike(int id, int userId) { try { @@ -98,25 +98,26 @@ public async Task DeleteLike(int id) if (comment == null) throw new ArgumentException($"Comment with id {id} not found"); - var userId = comment.UserId; + if (userId <= 0) + return BadRequest(new { message = "Invalid user id." }); - await _commentLikeService.DeleteLikeAsync(id, userId.Value); + await _commentLikeService.DeleteLikeAsync(id, userId); return NoContent(); } catch (ArgumentException ex) { - _logger.LogWarning(ex, "Like not found for comment id: {CommentId} and user", id); + _logger.LogWarning(ex, "Like not found for comment id: {CommentId} and user {UserId}", id, userId); return NotFound(new { message = ex.Message }); } catch (UnauthorizedAccessException ex) { - _logger.LogWarning(ex, "Unauthorized delete attempt for comment id: {CommentId}", id); + _logger.LogWarning(ex, "Unauthorized delete attempt for comment id: {CommentId} by user {UserId}", id, userId); return Forbid(); } catch (Exception ex) { - _logger.LogError(ex, "Error deleting like from comment id: {CommentId}", id); + _logger.LogError(ex, "Error deleting like from comment id: {CommentId} by user {UserId}", id, userId); return StatusCode(500, new { message = "An error occurred while deleting like" }); } } From c57f1a2ca4a3504542064bccaae12e75d6a51e0a Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 25 Aug 2025 05:48:58 +0300 Subject: [PATCH 40/92] FIX: handling null user exception on AddLikeAsync function --- AskFm/AskFm.BLL/Services/CommentLikeService.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs index b63d58c..e67aebe 100644 --- a/AskFm/AskFm.BLL/Services/CommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -93,6 +93,10 @@ public async Task AddLikeAsync(int commentId, int userId) var user = await _unitOfWork.Users.GetByIdAsync(userId); + + if (user == null) + throw new ArgumentException($"User with id {userId} not found"); + string userName = user.UserName; From f2b42a13ac383308a54e2781d52e9895f61639ba Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 25 Aug 2025 06:00:15 +0300 Subject: [PATCH 41/92] FIX: Added await and remove .Result from DeletLikeAsync function --- .../AskFm.BLL/Services/CommentLikeService.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs index e67aebe..eff7234 100644 --- a/AskFm/AskFm.BLL/Services/CommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -123,11 +123,14 @@ public async Task DeleteLikeAsync(int commentId, int userId) try { _logger.LogInformation("Deleting the comment like from user {userid} on comment id {commentId}", userId, commentId); - var commentLike = await _unitOfWork.CommentLikes.FindAsync(cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted); - - var comment = _unitOfWork.Comments.GetByIdAsync(commentId); - Console.WriteLine(comment.Result.Content); + var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); + + if(comment == null) + throw new ArgumentException($"User didn't like this comment"); + + + var commentLike = await _unitOfWork.CommentLikes.FindAsync(cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted); // if the user didn't like this comment before if(commentLike == null) @@ -135,16 +138,21 @@ public async Task DeleteLikeAsync(int commentId, int userId) await _unitOfWork.CommentLikes.RemoveAsync(commentLike); - comment.Result.IsDeleted = true; - comment.Result.CommentLikes.Remove(commentLike); - comment.Result.LikeCount--; + + if (comment.CommentLikes != null) + comment.CommentLikes.Remove(commentLike); + + if (comment.LikeCount > 0) + comment.LikeCount--; + + _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); } catch (Exception e) { - Console.WriteLine(e); + _logger.LogError(e, "Failed to delete like by user {UserId} on comment {CommentId}", userId, commentId); throw; } } From c30d997c95165b514ef78038a34d61abde81fe67 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 25 Aug 2025 06:05:02 +0300 Subject: [PATCH 42/92] test: Added the Async function calls tests to AddLike_WhenCommentNotFoundOrDeleted Test case --- AskFm/Tests/CommentLikeServiceTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AskFm/Tests/CommentLikeServiceTest.cs b/AskFm/Tests/CommentLikeServiceTest.cs index fec7191..8c55483 100644 --- a/AskFm/Tests/CommentLikeServiceTest.cs +++ b/AskFm/Tests/CommentLikeServiceTest.cs @@ -77,14 +77,14 @@ public async void AddLike_WhenCommentNotFoundOrDeleted_ThrowException() // Arrange var commentId = 1000; var userId = 5; - _mockCommentRepository.Setup(repo => repo.GetById(commentId)) - .Returns((Comment)null); + _mockCommentRepository.Setup(repo => repo.GetByIdAsync(commentId)) + .ReturnsAsync((Comment)null); // Assert await Assert.ThrowsAsync(() => _commentLikeService.AddLikeAsync(commentId, userId)); - _mockCommentLikeRepository.Verify(repo => repo.Add(It.IsAny()), Times.Never); - _mockUnitOfWork.Verify(uow => uow.Save(), Times.Never); + _mockCommentLikeRepository.Verify(repo => repo.AddAsync(It.IsAny()), Times.Never); + _mockUnitOfWork.Verify(uow => uow.SaveAsync(), Times.Never); } From 59b594b2fd5c543cf718081bb444c9af7ae8302c Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 25 Aug 2025 06:10:57 +0300 Subject: [PATCH 43/92] test: added await & async to DeleteLike test case --- AskFm/Tests/CommentLikeServiceTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AskFm/Tests/CommentLikeServiceTest.cs b/AskFm/Tests/CommentLikeServiceTest.cs index 8c55483..43a2a31 100644 --- a/AskFm/Tests/CommentLikeServiceTest.cs +++ b/AskFm/Tests/CommentLikeServiceTest.cs @@ -90,7 +90,7 @@ public async void AddLike_WhenCommentNotFoundOrDeleted_ThrowException() [Fact] - public void DeleteLike_WhenDeleteByAuthor_CommentIsDeleted() + public async void DeleteLike_WhenDeleteByAuthor_CommentIsDeleted() { var commentId = 1; var userId = 5; @@ -128,7 +128,7 @@ public void DeleteLike_WhenDeleteByAuthor_CommentIsDeleted() // Act - _commentLikeService.DeleteLikeAsync(commentId, userId); + await _commentLikeService.DeleteLikeAsync(commentId, userId); From 1a2aa440d79cd04b3868c35d0e5eb95abcc7b6ae Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 25 Aug 2025 06:12:36 +0300 Subject: [PATCH 44/92] test: Added Task return type for all test cases --- AskFm/Tests/CommentLikeServiceTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AskFm/Tests/CommentLikeServiceTest.cs b/AskFm/Tests/CommentLikeServiceTest.cs index 43a2a31..e5f8de8 100644 --- a/AskFm/Tests/CommentLikeServiceTest.cs +++ b/AskFm/Tests/CommentLikeServiceTest.cs @@ -72,7 +72,7 @@ public async Task AddLike_WhenCommentIsNotDeleted_AddingNewLikeOnTheComment() } [Fact] - public async void AddLike_WhenCommentNotFoundOrDeleted_ThrowException() + public async Task AddLike_WhenCommentNotFoundOrDeleted_ThrowException() { // Arrange var commentId = 1000; @@ -90,7 +90,7 @@ public async void AddLike_WhenCommentNotFoundOrDeleted_ThrowException() [Fact] - public async void DeleteLike_WhenDeleteByAuthor_CommentIsDeleted() + public async Task DeleteLike_WhenDeleteByAuthor_CommentIsDeleted() { var commentId = 1; var userId = 5; @@ -144,7 +144,7 @@ public async void DeleteLike_WhenDeleteByAuthor_CommentIsDeleted() [Fact] - public async void GetLikesForComment_WhenCommentLikesIsNotEmpty_ReturnsCommentLikesList() + public async Task GetLikesForComment_WhenCommentLikesIsNotEmpty_ReturnsCommentLikesList() { var commentId = 1; var userId = 4; @@ -199,7 +199,7 @@ public async void GetLikesForComment_WhenCommentLikesIsNotEmpty_ReturnsCommentLi [Fact] - public async void GetLikesForComment_WhenCommentLikesIsEmpty_ReturnsEmptyList() + public async Task GetLikesForComment_WhenCommentLikesIsEmpty_ReturnsEmptyList() { // Arrange var commentId = 1; From 19ef6500039fdf08cedd6b5c7833ed8e749cdfa5 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 25 Aug 2025 06:29:17 +0300 Subject: [PATCH 45/92] refactor: Added converted return type Task to Task to DeleteLikeAsync function --- AskFm/AskFm.BLL/Services/CommentLikeService.cs | 10 ++++++---- AskFm/AskFm.BLL/Services/ICommentLikeService.cs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs index eff7234..b238aee 100644 --- a/AskFm/AskFm.BLL/Services/CommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -118,7 +118,7 @@ public async Task AddLikeAsync(int commentId, int userId) } - public async Task DeleteLikeAsync(int commentId, int userId) + public async Task DeleteLikeAsync(int commentId, int userId) { try { @@ -130,7 +130,9 @@ public async Task DeleteLikeAsync(int commentId, int userId) throw new ArgumentException($"User didn't like this comment"); - var commentLike = await _unitOfWork.CommentLikes.FindAsync(cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted); + var commentLike = await + _unitOfWork.CommentLikes.FindAsync( + predicate: cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted); // if the user didn't like this comment before if(commentLike == null) @@ -145,10 +147,10 @@ public async Task DeleteLikeAsync(int commentId, int userId) if (comment.LikeCount > 0) comment.LikeCount--; - _unitOfWork.Comments.Update(comment); + _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); - + return true; } catch (Exception e) { diff --git a/AskFm/AskFm.BLL/Services/ICommentLikeService.cs b/AskFm/AskFm.BLL/Services/ICommentLikeService.cs index 712a4a4..d2496bd 100644 --- a/AskFm/AskFm.BLL/Services/ICommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/ICommentLikeService.cs @@ -6,5 +6,5 @@ public interface ICommentLikeService { Task> GetLikesForCommentAsync(int commentId); Task AddLikeAsync(int commentId, int userId); - Task DeleteLikeAsync(int commentId, int userId); + Task DeleteLikeAsync(int commentId, int userId); } \ No newline at end of file From 0ef96ce5d31b3bd2298033ab8b621e54732b6df1 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 25 Aug 2025 19:50:12 +0300 Subject: [PATCH 46/92] merge: merged Authintecation PR to the Comments branch --- AskFm/AskFm.API/AskFm.API.csproj | 1 + AskFm/AskFm.BLL/AskFm.BLL.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index c71e10c..77d8e4c 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -7,6 +7,7 @@ + diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index 6977b2a..09d480a 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -12,6 +12,7 @@ + From 90943a9a78d6c28e76a5d34163bb6d618de77d8a Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 25 Aug 2025 20:11:28 +0300 Subject: [PATCH 47/92] refactor: added dependancies injection of ICommentService and ICommentLikeService to program.cs --- AskFm/AskFm.API/Program.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index bf1d8f9..a45af9f 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -1,4 +1,5 @@ using System.Text; +using AskFm.BLL.Services; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; using AskFm.DAL; @@ -44,6 +45,8 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); From 75057f0d11358028782db0589c50a3a040244f88 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Tue, 26 Aug 2025 02:25:22 +0300 Subject: [PATCH 48/92] Add Default Values to CreatedAt, UpdatedAt and DeletedAt --- AskFm/AskFm.DAL/AppDbContext.cs | 9 +- ...0825225059_AddCreatedAtDefault.Designer.cs | 779 +++++++++++++++++ .../20250825225059_AddCreatedAtDefault.cs | 163 ++++ ...ddUpdatedAtAndDeletedAtDefault.Designer.cs | 811 ++++++++++++++++++ ...5230515_AddUpdatedAtAndDeletedAtDefault.cs | 307 +++++++ .../Migrations/AppDbContextModelSnapshot.cs | 96 ++- 6 files changed, 2138 insertions(+), 27 deletions(-) create mode 100644 AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs diff --git a/AskFm/AskFm.DAL/AppDbContext.cs b/AskFm/AskFm.DAL/AppDbContext.cs index 4f35d3d..7c9196c 100644 --- a/AskFm/AskFm.DAL/AppDbContext.cs +++ b/AskFm/AskFm.DAL/AppDbContext.cs @@ -32,13 +32,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entityType.ClrType).Property("DeletedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("UpdatedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("CreatedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("IsDeleted") .HasColumnType("BIT") diff --git a/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs new file mode 100644 index 0000000..66961a8 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs @@ -0,0 +1,779 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250825225059_AddCreatedAtDefault")] + partial class AddCreatedAtDefault + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs new file mode 100644 index 0000000..475bfac --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs @@ -0,0 +1,163 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class AddCreatedAtDefault : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs new file mode 100644 index 0000000..443269e --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs @@ -0,0 +1,811 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250825230515_AddUpdatedAtAndDeletedAtDefault")] + partial class AddUpdatedAtAndDeletedAtDefault + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs new file mode 100644 index 0000000..5e8c59e --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs @@ -0,0 +1,307 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class AddUpdatedAtAndDeletedAtDefault : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index bdfe5a3..328f401 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -50,10 +50,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("Email") .IsRequired() @@ -113,7 +117,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserName") .IsRequired() @@ -150,10 +156,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(1000)"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -170,7 +180,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserId") .HasColumnType("int"); @@ -195,10 +207,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -206,7 +222,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("UserId", "CommentId"); @@ -224,10 +242,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsActive") .HasColumnType("bit"); @@ -238,7 +260,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("FollowerId", "FollowedId"); @@ -256,10 +280,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -280,7 +308,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserId") .HasColumnType("int"); @@ -301,10 +331,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -312,7 +346,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("SavedThreadId", "UserId"); @@ -341,10 +377,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -361,7 +401,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("isAnonymous") .HasColumnType("bit"); @@ -384,10 +426,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -395,7 +441,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("ThreadId", "UserId"); From 208976beb58c22d86d1e93b9b7464169c5022247 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Tue, 26 Aug 2025 02:25:22 +0300 Subject: [PATCH 49/92] Add Default Values to CreatedAt, UpdatedAt and DeletedAt --- AskFm/AskFm.DAL/AppDbContext.cs | 9 +- ...0825225059_AddCreatedAtDefault.Designer.cs | 779 +++++++++++++++++ .../20250825225059_AddCreatedAtDefault.cs | 163 ++++ ...ddUpdatedAtAndDeletedAtDefault.Designer.cs | 811 ++++++++++++++++++ ...5230515_AddUpdatedAtAndDeletedAtDefault.cs | 307 +++++++ .../Migrations/AppDbContextModelSnapshot.cs | 96 ++- 6 files changed, 2138 insertions(+), 27 deletions(-) create mode 100644 AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs diff --git a/AskFm/AskFm.DAL/AppDbContext.cs b/AskFm/AskFm.DAL/AppDbContext.cs index 4f35d3d..7c9196c 100644 --- a/AskFm/AskFm.DAL/AppDbContext.cs +++ b/AskFm/AskFm.DAL/AppDbContext.cs @@ -32,13 +32,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entityType.ClrType).Property("DeletedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("UpdatedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("CreatedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("IsDeleted") .HasColumnType("BIT") diff --git a/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs new file mode 100644 index 0000000..66961a8 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs @@ -0,0 +1,779 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250825225059_AddCreatedAtDefault")] + partial class AddCreatedAtDefault + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs new file mode 100644 index 0000000..475bfac --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs @@ -0,0 +1,163 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class AddCreatedAtDefault : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs new file mode 100644 index 0000000..443269e --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs @@ -0,0 +1,811 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250825230515_AddUpdatedAtAndDeletedAtDefault")] + partial class AddUpdatedAtAndDeletedAtDefault + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs new file mode 100644 index 0000000..5e8c59e --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs @@ -0,0 +1,307 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class AddUpdatedAtAndDeletedAtDefault : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index 796abaa..832c22c 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -50,10 +50,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("Email") .IsRequired() @@ -113,7 +117,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserName") .IsRequired() @@ -150,10 +156,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(1000)"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -170,7 +180,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserId") .HasColumnType("int"); @@ -195,10 +207,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -206,7 +222,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("UserId", "CommentId"); @@ -224,10 +242,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsActive") .ValueGeneratedOnAdd() @@ -240,7 +262,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("FollowerId", "FollowedId"); @@ -258,10 +282,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -272,7 +300,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserId") .HasColumnType("int"); @@ -300,10 +330,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -311,7 +345,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("SavedThreadId", "UserId"); @@ -340,10 +376,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -360,7 +400,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("isAnonymous") .HasColumnType("bit"); @@ -383,10 +425,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -394,7 +440,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("ThreadId", "UserId"); From 01e21ff830f0b823befee311bd960177f8035eaa Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Wed, 27 Aug 2025 21:39:18 +0300 Subject: [PATCH 50/92] refactor: edited the current user implementation on UserService and the mapping on 'profile' endpoint --- AskFm/AskFm.API/Controllers/UserController.cs | 15 ++++++++++++--- .../Services/UserIdentityService/IUserService.cs | 3 ++- .../Services/UserIdentityService/UserService.cs | 14 +++----------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 87a3a97..381dc74 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -37,16 +37,25 @@ public async Task GetUsers() } [HttpGet] - [Route("current-user")] + [Route("profile")] public async Task GetCurrentUserAsync() { var result = await _userService.GetCurrentUserAsync(); - + if (!result.success) { return BadRequest(result.Errors); } - return Ok(result.Data); + ReadUserDTO readUserDTO = new ReadUserDTO() + { + Name = result.Data.Name, + Email = result.Data.Email, + AvatarPath = result.Data.AvatarPath, + Bio = result.Data.Bio, + followerCount = result.Data.FollowersCount, + LastSeen = result.Data.LastSeen, + }; + return Ok(readUserDTO); } /* GET Users only for now diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs index 51a9e21..93b7796 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -1,4 +1,5 @@ using AskFm.BLL.DTO.UserDTOs; +using AskFm.DAL.Models; namespace AskFm.BLL.Services.UserIdentityService; @@ -10,7 +11,7 @@ public interface IUserService Task> UnfollowUserAsync(int followerId, int targetUserId); Task UpdateLastSeenAsync(int userId, DateTime lastSeen); Task> GetUserByIdAsync(int userId); - Task> GetCurrentUserAsync(); + Task> GetCurrentUserAsync(); Task> ResetPassword(string newPassword); Task> ConfirmEmail(); diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 9169703..8decd39 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -52,7 +52,7 @@ public Task> GetUserByIdAsync(int userId) throw new NotImplementedException(); } - public async Task> GetCurrentUserAsync() + public async Task> GetCurrentUserAsync() { string email = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.Email).Value; if (string.IsNullOrEmpty(email)) @@ -61,19 +61,11 @@ public async Task> GetCurrentUserAsync() { "Can't Access Current user" }; - return await ServiceResult.Failure(errors); + return await ServiceResult.Failure(errors); } var currentAppUser = await _userManager.FindByEmailAsync(email); - return await ServiceResult.Success(new ReadUserDTO() - { - Name = currentAppUser.Name, - Email = currentAppUser.Email, - LastSeen = currentAppUser.LastSeen, - Bio = currentAppUser.Bio, - AvatarPath = currentAppUser.AvatarPath, - followerCount = currentAppUser.FollowersCount - }); + return await ServiceResult.Success(currentAppUser); } public Task> ResetPassword(string newPassword) From dcc35e80ff5cb76b8b1fc0c260afad931494d873 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Thu, 28 Aug 2025 04:08:24 +0300 Subject: [PATCH 51/92] Finish notification unit test and update notification hub --- AskFm/AskFm.API/Program.cs | 90 ++++- AskFm/AskFm.BLL/Hub/NotificationHub.cs | 20 +- .../AskFm.BLL/Services/NotificationService.cs | 22 +- AskFm/Tests/NotificationServiceTests.cs | 340 ++++++++++++++++++ AskFm/Tests/Tests.csproj | 5 + 5 files changed, 455 insertions(+), 22 deletions(-) create mode 100644 AskFm/Tests/NotificationServiceTests.cs diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 34b6b1f..70e6f78 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -8,8 +8,14 @@ using DotNetEnv; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore.Proxies; -namespace AskFm.API; +using AskFm.BLL.Hub; +using AskFm.BLL.Services; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using AskFm.BLL.Services.UserIdentityService; +namespace AskFm.API; public class Program { @@ -18,7 +24,6 @@ public static void Main(string[] args) var builder = WebApplication.CreateBuilder(args); // Add services to the container. - builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); @@ -29,6 +34,7 @@ public static void Main(string[] args) { throw new Exception("Connection string is null"); } + builder.Services.AddHttpContextAccessor(); builder.Services.AddDbContext(options => options @@ -36,13 +42,14 @@ public static void Main(string[] args) .UseSqlServer(ConnectionString)); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - JwtOptions jwtOptions = new JwtOptions { Issuer = Environment.GetEnvironmentVariable("ISSUER"), @@ -56,6 +63,39 @@ public static void Main(string[] args) throw new Exception("jwtOptions is null"); } + // Enhanced SignalR Configuration + builder.Services.AddSignalR(options => + { + options.EnableDetailedErrors = true; + options.KeepAliveInterval = TimeSpan.FromSeconds(15); + options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); + options.HandshakeTimeout = TimeSpan.FromSeconds(15); + }); + + // CORS Configuration for SignalR + builder.Services.AddCors(options => + { + options.AddPolicy("SignalRPolicy", policy => + { + // Option 1: Allow any origin (for development only) + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + + // Option 2: Specific origins (uncomment and modify when you know frontend URLs) + // policy.WithOrigins( + // "http://localhost:3000", // React default + // "http://localhost:4200", // Angular default + // "http://localhost:8080", // Vue default + // "http://localhost:5173", // Vite default + // "https://yourdomain.com" // Production domain + // ) + // .AllowAnyMethod() + // .AllowAnyHeader() + // .AllowCredentials(); + }); + }); + builder.Services.Configure(Options => { Options.Issuer = Environment.GetEnvironmentVariable("ISSUER"); @@ -69,9 +109,8 @@ public static void Main(string[] args) { options.DefaultAuthenticateScheme = "Bearer"; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer( Options => + .AddJwtBearer(Options => { Options.TokenValidationParameters = new TokenValidationParameters { @@ -85,7 +124,23 @@ public static void Main(string[] args) ClockSkew = TimeSpan.FromMinutes(0) }; + // Enable JWT authentication for SignalR + Options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/notificationHub")) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; }); + builder.Services.AddAuthorization(); builder.Services.AddIdentity>(options => { @@ -108,30 +163,33 @@ public static void Main(string[] args) options.SignIn.RequireConfirmedEmail = false; options.SignIn.RequireConfirmedAccount = false; options.SignIn.RequireConfirmedPhoneNumber = false; - /* - * close confirmed email imediatly in register, - * but in other scenario we will block some action untill the user verify his email - */ }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - - builder.Services.AddScoped(); - - var app = builder.Build(); // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) app.MapOpenApi(); + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/openapi/v1.json", "api"); + }); + } app.UseHttpsRedirection(); + + // Apply CORS before authentication + app.UseCors("SignalRPolicy"); + app.UseAuthentication(); app.UseAuthorization(); - app.MapControllers(); + // Map SignalR Hub app.MapHub("/notificationHub"); app.Run(); diff --git a/AskFm/AskFm.BLL/Hub/NotificationHub.cs b/AskFm/AskFm.BLL/Hub/NotificationHub.cs index e881fcd..500526f 100644 --- a/AskFm/AskFm.BLL/Hub/NotificationHub.cs +++ b/AskFm/AskFm.BLL/Hub/NotificationHub.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Authorization; namespace AskFm.BLL.Hub { + [Authorize] public class NotificationHub : Microsoft.AspNetCore.SignalR.Hub { public async Task JoinUserGroup(string userId) @@ -14,8 +16,24 @@ public async Task LeaveUserGroup(string userId) await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user_{userId}"); } - public override async Task OnDisconnectedAsync(Exception exception) + public override async Task OnConnectedAsync() { + // Auto-join user to their group based on their ID from JWT token + var userId = Context.UserIdentifier; + if (!string.IsNullOrEmpty(userId)) + { + await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}"); + } + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var userId = Context.UserIdentifier; + if (!string.IsNullOrEmpty(userId)) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user_{userId}"); + } await base.OnDisconnectedAsync(exception); } } diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index d94ea4f..13b78a5 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -111,20 +111,20 @@ public async Task MarkNotificationAsRead(int notificationId) notification.IsRead = true; _unitOfWork.Notifications.Update(notification); await _unitOfWork.SaveAsync(); - + return "notification has been read"; } public async Task MarkAllNotificationsAsRead(int userId) { var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.IsRead); - + foreach (var notification in unreadNotifications) { notification.IsRead = true; _unitOfWork.Notifications.Update(notification); } - + await _unitOfWork.SaveAsync(); return "All notifications marked as read"; } @@ -144,7 +144,10 @@ public async Task CreateNotification(int userId, NotificationStatus type, int re await _unitOfWork.Notifications.AddAsync(notification); await _unitOfWork.SaveAsync(); - + + // Get actor information for the notification + var actorUser = await _notificationRepository.GetActorUserByResourceId(resourceId, type); + var notificationDto = new NotificationDto { Id = notification.Id, @@ -153,10 +156,19 @@ public async Task CreateNotification(int userId, NotificationStatus type, int re ResourceId = notification.ResourceId, Message = notification.Message, IsRead = notification.IsRead, - CreatedAt = notification.CreatedAt + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser.UserName ?? "Unknown", + AvatarPath = actorUser.AvatarPath ?? string.Empty + } }; + // Send real-time notification to the specific user await _hubContext.Clients.Group($"user_{userId}") .SendAsync("ReceiveNotification", notificationDto); } + + } \ No newline at end of file diff --git a/AskFm/Tests/NotificationServiceTests.cs b/AskFm/Tests/NotificationServiceTests.cs new file mode 100644 index 0000000..6a16c64 --- /dev/null +++ b/AskFm/Tests/NotificationServiceTests.cs @@ -0,0 +1,340 @@ +using AskFm.BLL.DTO; +using AskFm.BLL.Hub; +using AskFm.DAL.Enums; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using Microsoft.AspNetCore.Mvc.Diagnostics; +using Microsoft.AspNetCore.SignalR; +using Moq; +using Xunit; + +namespace AskFm.BLL.Tests.Services +{ + public class NotificationServiceTests + { + private readonly Mock _notificationRepositoryMock; + private readonly Mock _unitOfWorkMock; + private readonly Mock> _hubContextMock; + private readonly Mock _mockClients; + private readonly Mock _mockClientProxy; + private readonly Mock _mockGroups; + private readonly NotificationService _notificationService; + + public NotificationServiceTests() + { + _notificationRepositoryMock = new Mock(); + _unitOfWorkMock = new Mock(); + _hubContextMock = new Mock>(); + _mockClients = new Mock(); + _mockClientProxy = new Mock(); + _mockGroups = new Mock(); + + // Setup hub context relationships + _hubContextMock.Setup(p => p.Clients).Returns(_mockClients.Object); + _hubContextMock.Setup(p => p.Groups).Returns(_mockGroups.Object); + _mockClients.Setup(p => p.Group(It.IsAny())).Returns(_mockClientProxy.Object); + + _notificationService = new NotificationService( + _notificationRepositoryMock.Object, + _unitOfWorkMock.Object, + _hubContextMock.Object + ); + } + + [Fact] + public async Task GetUserNotifications_ReturnsCorrectDtoWithPagination() + { + // Arrange + + int userId = 1; + int pageNumber = 1; + int pageSize = 10; + var notifications = new List + { + new Notification + { + Id = 1, + UserId = userId, + Type = NotificationStatus.QUESTION, + ResourceId = 100, + Message = "Test notification", + IsRead = false, + CreatedAt = DateTime.UtcNow + } + }; + + var totalCount = 1; + var actorUser = new ApplicationUser { Id = 2, UserName = "test_user", AvatarPath = "test.jpg" }; + + _notificationRepositoryMock.Setup(p => p.GetAllNotifications(userId, pageNumber, pageSize)).ReturnsAsync((notifications, totalCount)); + _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.QUESTION)).ReturnsAsync(actorUser); + + // Act + + var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); + + // Assert + + Assert.Single(result); + Assert.Equal(1, result[0].Id); + Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); + Assert.Equal("Test notification", result[0].Message); + Assert.False(result[0].IsRead); + Assert.Equal("test_user", result[0].Actor.Username); + Assert.Equal("test.jpg", result[0].Actor.AvatarPath); + Assert.Equal(2, result[0].Actor.Id); + Assert.Equal(1, result[0].Pagination.TotalCount); + + } + + [Fact] + public async Task GetUserNotifications_WithNullActor_ReturnsCorrectDtoWithPagination() + { + // Arrange + + int userId = 1; + int pageNumber = 1; + int pageSize = 10; + var notifications = new List + { + new Notification + { + Id = 1, + UserId = userId, + Type = NotificationStatus.QUESTION, + ResourceId = 100, + Message = "Test notification", + IsRead = false, + CreatedAt = DateTime.UtcNow + } + }; + + var totalCount = 1; + + _notificationRepositoryMock.Setup(p => p.GetAllNotifications(userId, pageNumber, pageSize)).ReturnsAsync((notifications, totalCount)); + _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.QUESTION)).ReturnsAsync((ApplicationUser)null); + + // Act + + var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); + + // Assert + + Assert.Single(result); + Assert.Equal(1, result[0].Id); + Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); + Assert.Equal("Test notification", result[0].Message); + Assert.False(result[0].IsRead); + Assert.Null(result[0].Actor); + Assert.Equal(1, result[0].Pagination.TotalCount); + + } + + [Fact] + public async Task GetNotificationsByType_WithValidCategory_ReturnsFilteredNotifications() + { + // Arrange + + int userId = 1; + string category = "answer"; + int pageNumber = 1; + int pageSize = 10; + var notifications = new List + { + new Notification + { + Id = 1, + UserId = userId, + Type = NotificationStatus.ANSWER, + ResourceId = 100, + Message = "Test notification", + IsRead = false, + CreatedAt = DateTime.UtcNow + } + }; + + var totalCount = 1; + var actorUser = new ApplicationUser { Id = 2, UserName = "test_user", AvatarPath = "test.jpg" }; + + _notificationRepositoryMock.Setup(p => p.GetNotificationsByType(userId, NotificationStatus.ANSWER, pageNumber, pageSize)).ReturnsAsync((notifications, totalCount)); + _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.ANSWER)).ReturnsAsync(actorUser); + + // Act + + var result = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); + + // Assert + + Assert.Single(result); + Assert.Equal(1, result[0].Id); + Assert.Equal(category.ToUpper(), result[0].Type); + Assert.Equal("Test notification", result[0].Message); + Assert.False(result[0].IsRead); + Assert.Equal("test_user", result[0].Actor.Username); + Assert.Equal("test.jpg", result[0].Actor.AvatarPath); + Assert.Equal(2, result[0].Actor.Id); + Assert.Equal(1, result[0].Pagination.TotalCount); + + } + + [Fact] + public async Task GetNotificationsByType_WithINValidCategory_ReturnsFilteredNotifications() + { + // Arrang + int userId = 1; + string category = "InvalidCategory"; + // Act and Assert + + var ex = await Assert.ThrowsAsync(() => _notificationService.GetNotificationsByType(userId, category)); + Assert.Contains("Invalid notification category", ex.Message); + } + + [Fact] + public async Task MarkNotificationAsRead_WithValidId_UpdatesNotification() + { + // Arrange + + int notificationId = 1; + Notification notification = new Notification { Id = notificationId, UserId = 1, IsRead = false }; + _unitOfWorkMock.Setup(p => p.Notifications.GetByIdAsync(notificationId)).ReturnsAsync(notification); + _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); + + // Act + var result = await _notificationService.MarkNotificationAsRead(notificationId); + + // Assert + + Assert.True(notification.IsRead); + Assert.Equal("notification has been read", result); + _unitOfWorkMock.Verify(p => p.Notifications.Update(notification), Times.Once); + _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + } + + [Fact] + public async Task MarkNotificationAsRead_WithInvalidId_ThrowsInvalidOperationException() + { + // Arrange + var notificationId = 999; + _unitOfWorkMock.Setup(p => p.Notifications.GetByIdAsync(notificationId)).ReturnsAsync((Notification)null); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => _notificationService.MarkNotificationAsRead(notificationId)); + + Assert.Contains("Notification with ID 999 not found", ex.Message); + } + + [Fact] + public async Task MarkAllNotificationsAsRead_UpdatesAllUnreadNotifications() + { + // Arrange + var userId = 1; + var unreadNotifications = new List + { + new Notification { Id = 1, UserId = userId, IsRead = false }, + new Notification { Id = 2, UserId = userId, IsRead = false } + }; + + _unitOfWorkMock.Setup(p => p.Notifications.FindAllAsync(It.IsAny>>(), It.IsAny())) + .ReturnsAsync(unreadNotifications); + _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); + + // Act + var result = await _notificationService.MarkAllNotificationsAsRead(userId); + + // Assert + Assert.Equal("All notifications marked as read", result); + Assert.True(unreadNotifications.All(n => n.IsRead)); + _unitOfWorkMock.Verify(p => p.Notifications.Update(It.IsAny()), Times.Exactly(2)); + _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + } + + [Fact] + public async Task CreateNotification_WithValidData_CreatesNotificationAndSendsSignalR() + { + // Arrange + var userId = 1; + var type = NotificationStatus.QUESTION; + var resourceId = 100; + var message = "Test notification message"; + var actorUser = new ApplicationUser + { + Id = 2, + UserName = "test_actor", + AvatarPath = "actor.jpg" + }; + + // Setup specific mock behavior for this test + _mockClients.Setup(p => p.Group($"user_{userId}")).Returns(_mockClientProxy.Object); + + _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny())) + .Returns(Task.CompletedTask); + _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); + + _notificationRepositoryMock.Setup(r => r.GetActorUserByResourceId(resourceId, type)) + .ReturnsAsync(actorUser); + + // Act + await _notificationService.CreateNotification(userId, type, resourceId, message); + + // Assert + // Verify notification was added to database + _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.Is(n => + n.UserId == userId && + n.Type == type && + n.ResourceId == resourceId && + n.Message == message && + !n.IsRead && + n.CreatedAt != default && + n.UpdatedAt != default + )), Times.Once); + + // Verify database save was called + _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + + // Verify actor user was fetched + _notificationRepositoryMock.Verify(r => r.GetActorUserByResourceId(resourceId, type), Times.Once); + + // Verify SignalR notification was sent to correct group + _mockClientProxy.Verify(cp => cp.SendCoreAsync( + "ReceiveNotification", + It.Is(args => args.Length == 1 && + ((NotificationDto)args[0]).UserId == userId && + ((NotificationDto)args[0]).Type == type.ToString() && + ((NotificationDto)args[0]).Message == message && + ((NotificationDto)args[0]).Actor.Username == "test_actor" && + ((NotificationDto)args[0]).Actor.AvatarPath == "actor.jpg" + ), + default(CancellationToken) + ), Times.Once); + } + + [Fact] + public async Task CreateNotification_WhenDatabaseSaveFails_ThrowsException() + { + // Arrange + var userId = 1; + var type = NotificationStatus.ANSWER; + var resourceId = 300; + var message = "Answer notification"; + + _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny())) + .Returns(Task.CompletedTask); + _unitOfWorkMock.Setup(p => p.SaveAsync()) + .ThrowsAsync(new Exception("Database error")); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => _notificationService.CreateNotification(userId, type, resourceId, message) + ); + + Assert.Equal("Database error", exception.Message); + + // Verify notification was attempted to be added + _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.IsAny()), Times.Once); + _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + } + } + + +} \ No newline at end of file diff --git a/AskFm/Tests/Tests.csproj b/AskFm/Tests/Tests.csproj index 95d873d..ef29c88 100644 --- a/AskFm/Tests/Tests.csproj +++ b/AskFm/Tests/Tests.csproj @@ -10,6 +10,7 @@ + @@ -18,4 +19,8 @@ + + + + From 73b3fbaabbd7c28fe32deaa3b3fd6f6cdbae5d36 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Thu, 28 Aug 2025 05:41:48 +0300 Subject: [PATCH 52/92] Apply Auth and update all files that depend on it --- .../Controllers/NotificationController.cs | 54 ++++-- .../Services/INotificationService.cs | 2 +- .../AskFm.BLL/Services/NotificationService.cs | 6 +- .../Interfaces/INotificationRepository.cs | 1 + .../Repositories/NotificationRepository.cs | 19 +- AskFm/Tests/NotificationServiceTests.cs | 169 +++++++----------- 6 files changed, 126 insertions(+), 125 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs index 5630476..9271ecd 100644 --- a/AskFm/AskFm.API/Controllers/NotificationController.cs +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -1,11 +1,13 @@ using AskFm.BLL.Services; using AskFm.DAL.Enums; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace AskFm.API.Controllers { [ApiController] [Route("api/[controller]")] + [Authorize] public class NotificationController : ControllerBase { private readonly INotificationService _notificationService; @@ -15,11 +17,12 @@ public NotificationController(INotificationService notificationService) _notificationService = notificationService; } - [HttpGet("{userId}")] - public async Task GetUserNotifications(int userId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + [HttpGet] + public async Task GetUserNotifications([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) { try { + var userId = GetCurrentUserId(); var notifications = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); return Ok(notifications); } @@ -29,11 +32,12 @@ public async Task GetUserNotifications(int userId, [FromQuery] in } } - [HttpGet("{userId}/type/{category}")] - public async Task GetNotificationsByType(int userId, string category, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + [HttpGet("type/{category}")] + public async Task GetNotificationsByType(string category, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) { try { + var userId = GetCurrentUserId(); var response = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); return Ok(response); } @@ -52,20 +56,30 @@ public async Task MarkNotificationAsRead(int notificationId) { try { - var result = await _notificationService.MarkNotificationAsRead(notificationId); + var userId = GetCurrentUserId(); + var result = await _notificationService.MarkNotificationAsRead(notificationId, userId); return Ok(new { message = result }); } + catch (InvalidOperationException ex) + { + return NotFound(ex.Message); + } + catch (UnauthorizedAccessException ex) + { + return Forbid(ex.Message); + } catch (Exception ex) { return BadRequest(ex.Message); } } - [HttpPut("{userId}/read-all")] - public async Task MarkAllNotificationsAsRead(int userId) + [HttpPut("read-all")] + public async Task MarkAllNotificationsAsRead() { try { + var userId = GetCurrentUserId(); var result = await _notificationService.MarkAllNotificationsAsRead(userId); return Ok(new { message = result }); } @@ -76,18 +90,36 @@ public async Task MarkAllNotificationsAsRead(int userId) } [HttpPost] - public async Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message) + [Authorize(Roles = "Admin")] + public async Task CreateNotification([FromBody] CreateNotificationRequest request) { try { - await _notificationService.CreateNotification(userId, type, resourceId, message); - - return CreatedAtAction(nameof(GetUserNotifications), new { userId = userId }, new { message = "Notification created successfully" }); + await _notificationService.CreateNotification(request.UserId, request.Type, request.ResourceId, request.Message); + return Ok(new { message = "Notification created successfully" }); } catch (Exception ex) { return BadRequest(ex.Message); } } + + private int GetCurrentUserId() + { + var userIdClaim = User.FindFirst("UserId")?.Value; + if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out int userId)) + { + throw new UnauthorizedAccessException("Invalid user token"); + } + return userId; + } + } + + public class CreateNotificationRequest + { + public int UserId { get; set; } + public NotificationStatus Type { get; set; } + public int ResourceId { get; set; } + public string Message { get; set; } } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs index 4eb9d63..6f75ea9 100644 --- a/AskFm/AskFm.BLL/Services/INotificationService.cs +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -7,7 +7,7 @@ public interface INotificationService { Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); Task> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); - Task MarkNotificationAsRead(int notificationId); + Task MarkNotificationAsRead(int notificationId, int userId); Task MarkAllNotificationsAsRead(int userId); Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 13b78a5..598f686 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -102,11 +102,11 @@ public async Task> GetNotificationsByType(int userId, stri return notificationDtos; } - public async Task MarkNotificationAsRead(int notificationId) + public async Task MarkNotificationAsRead(int notificationId, int userId) { - var notification = await _unitOfWork.Notifications.GetByIdAsync(notificationId); + var notification = await _notificationRepository.GetUserNotificationById(notificationId, userId); if (notification == null) - throw new InvalidOperationException($"Notification with ID {notificationId} not found."); + throw new InvalidOperationException("Notification not found or access denied."); notification.IsRead = true; _unitOfWork.Notifications.Update(notification); diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs index 816c2f1..310ec78 100644 --- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -8,4 +8,5 @@ public interface INotificationRepository Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize); Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); Task GetActorUserByResourceId(int resourceId, NotificationStatus type); + Task GetUserNotificationById(int notificationId, int userId); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs index d49564d..56189c6 100644 --- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -13,34 +13,34 @@ public NotificationRepository(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } - + public async Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize) { var query = _unitOfWork.Notifications.FindAll(n => n.UserId == userId); - + var totalCount = await query.CountAsync(); - + var notifications = await query .OrderByDescending(n => n.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); - + return (notifications, totalCount); } - + public async Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) { var query = _unitOfWork.Notifications.FindAll(n => n.UserId == userId && n.Type == status); var totalCount = await query.CountAsync(); - + var notifications = await query .OrderByDescending(n => n.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); - + return (notifications, totalCount); } @@ -78,4 +78,9 @@ public NotificationRepository(IUnitOfWork unitOfWork) } return null; } + + public async Task GetUserNotificationById(int notificationId, int userId) + { + return await _unitOfWork.Notifications.FindAsync(n => n.Id == notificationId && n.UserId == userId); + } } \ No newline at end of file diff --git a/AskFm/Tests/NotificationServiceTests.cs b/AskFm/Tests/NotificationServiceTests.cs index 6a16c64..dc830ce 100644 --- a/AskFm/Tests/NotificationServiceTests.cs +++ b/AskFm/Tests/NotificationServiceTests.cs @@ -1,9 +1,9 @@ using AskFm.BLL.DTO; using AskFm.BLL.Hub; +using AskFm.BLL.Services; using AskFm.DAL.Enums; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; -using Microsoft.AspNetCore.Mvc.Diagnostics; using Microsoft.AspNetCore.SignalR; using Moq; using Xunit; @@ -34,6 +34,10 @@ public NotificationServiceTests() _hubContextMock.Setup(p => p.Groups).Returns(_mockGroups.Object); _mockClients.Setup(p => p.Group(It.IsAny())).Returns(_mockClientProxy.Object); + // Setup unit of work notifications repository mock + var notificationRepoMock = new Mock>(); + _unitOfWorkMock.Setup(p => p.Notifications).Returns(notificationRepoMock.Object); + _notificationService = new NotificationService( _notificationRepositoryMock.Object, _unitOfWorkMock.Object, @@ -45,7 +49,6 @@ public NotificationServiceTests() public async Task GetUserNotifications_ReturnsCorrectDtoWithPagination() { // Arrange - int userId = 1; int pageNumber = 1; int pageSize = 10; @@ -70,11 +73,9 @@ public async Task GetUserNotifications_ReturnsCorrectDtoWithPagination() _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.QUESTION)).ReturnsAsync(actorUser); // Act - var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); // Assert - Assert.Single(result); Assert.Equal(1, result[0].Id); Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); @@ -84,14 +85,12 @@ public async Task GetUserNotifications_ReturnsCorrectDtoWithPagination() Assert.Equal("test.jpg", result[0].Actor.AvatarPath); Assert.Equal(2, result[0].Actor.Id); Assert.Equal(1, result[0].Pagination.TotalCount); - } [Fact] public async Task GetUserNotifications_WithNullActor_ReturnsCorrectDtoWithPagination() { // Arrange - int userId = 1; int pageNumber = 1; int pageSize = 10; @@ -115,11 +114,9 @@ public async Task GetUserNotifications_WithNullActor_ReturnsCorrectDtoWithPagina _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.QUESTION)).ReturnsAsync((ApplicationUser)null); // Act - var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); // Assert - Assert.Single(result); Assert.Equal(1, result[0].Id); Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); @@ -127,14 +124,12 @@ public async Task GetUserNotifications_WithNullActor_ReturnsCorrectDtoWithPagina Assert.False(result[0].IsRead); Assert.Null(result[0].Actor); Assert.Equal(1, result[0].Pagination.TotalCount); - } [Fact] public async Task GetNotificationsByType_WithValidCategory_ReturnsFilteredNotifications() { // Arrange - int userId = 1; string category = "answer"; int pageNumber = 1; @@ -160,11 +155,9 @@ public async Task GetNotificationsByType_WithValidCategory_ReturnsFilteredNotifi _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.ANSWER)).ReturnsAsync(actorUser); // Act - var result = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); // Assert - Assert.Single(result); Assert.Equal(1, result[0].Id); Assert.Equal(category.ToUpper(), result[0].Type); @@ -174,18 +167,19 @@ public async Task GetNotificationsByType_WithValidCategory_ReturnsFilteredNotifi Assert.Equal("test.jpg", result[0].Actor.AvatarPath); Assert.Equal(2, result[0].Actor.Id); Assert.Equal(1, result[0].Pagination.TotalCount); - } [Fact] - public async Task GetNotificationsByType_WithINValidCategory_ReturnsFilteredNotifications() + public async Task GetNotificationsByType_WithInvalidCategory_ThrowsArgumentException() { - // Arrang + // Arrange int userId = 1; string category = "InvalidCategory"; - // Act and Assert + int pageNumber = 1; + int pageSize = 10; - var ex = await Assert.ThrowsAsync(() => _notificationService.GetNotificationsByType(userId, category)); + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize)); Assert.Contains("Invalid notification category", ex.Message); } @@ -193,17 +187,28 @@ public async Task GetNotificationsByType_WithINValidCategory_ReturnsFilteredNoti public async Task MarkNotificationAsRead_WithValidId_UpdatesNotification() { // Arrange - int notificationId = 1; - Notification notification = new Notification { Id = notificationId, UserId = 1, IsRead = false }; - _unitOfWorkMock.Setup(p => p.Notifications.GetByIdAsync(notificationId)).ReturnsAsync(notification); + int userId = 1; + Notification notification = new Notification + { + Id = notificationId, + UserId = userId, + IsRead = false, + Type = NotificationStatus.QUESTION, + ResourceId = 100, + Message = "Test notification", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + _notificationRepositoryMock.Setup(p => p.GetUserNotificationById(notificationId, userId)) + .ReturnsAsync(notification); _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); // Act - var result = await _notificationService.MarkNotificationAsRead(notificationId); + var result = await _notificationService.MarkNotificationAsRead(notificationId, userId); // Assert - Assert.True(notification.IsRead); Assert.Equal("notification has been read", result); _unitOfWorkMock.Verify(p => p.Notifications.Update(notification), Times.Once); @@ -215,27 +220,49 @@ public async Task MarkNotificationAsRead_WithInvalidId_ThrowsInvalidOperationExc { // Arrange var notificationId = 999; - _unitOfWorkMock.Setup(p => p.Notifications.GetByIdAsync(notificationId)).ReturnsAsync((Notification)null); + var userId = 1; + _notificationRepositoryMock.Setup(p => p.GetUserNotificationById(notificationId, userId)) + .ReturnsAsync((Notification)null); // Act & Assert var ex = await Assert.ThrowsAsync( - () => _notificationService.MarkNotificationAsRead(notificationId)); + () => _notificationService.MarkNotificationAsRead(notificationId, userId)); - Assert.Contains("Notification with ID 999 not found", ex.Message); + Assert.Contains("Notification not found or access denied", ex.Message); } [Fact] public async Task MarkAllNotificationsAsRead_UpdatesAllUnreadNotifications() { // Arrange - var userId = 1; + int userId = 1; var unreadNotifications = new List { - new Notification { Id = 1, UserId = userId, IsRead = false }, - new Notification { Id = 2, UserId = userId, IsRead = false } + new Notification + { + Id = 1, + UserId = userId, + IsRead = false, + Type = NotificationStatus.QUESTION, + ResourceId = 100, + Message = "Test notification 1", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }, + new Notification + { + Id = 2, + UserId = userId, + IsRead = false, + Type = NotificationStatus.ANSWER, + ResourceId = 200, + Message = "Test notification 2", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + } }; - _unitOfWorkMock.Setup(p => p.Notifications.FindAllAsync(It.IsAny>>(), It.IsAny())) + _unitOfWorkMock.Setup(p => p.Notifications.FindAllAsync(It.IsAny>>(), null)) .ReturnsAsync(unreadNotifications); _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); @@ -244,97 +271,33 @@ public async Task MarkAllNotificationsAsRead_UpdatesAllUnreadNotifications() // Assert Assert.Equal("All notifications marked as read", result); - Assert.True(unreadNotifications.All(n => n.IsRead)); + Assert.All(unreadNotifications, n => Assert.True(n.IsRead)); _unitOfWorkMock.Verify(p => p.Notifications.Update(It.IsAny()), Times.Exactly(2)); _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); } [Fact] - public async Task CreateNotification_WithValidData_CreatesNotificationAndSendsSignalR() + public async Task CreateNotification_CreatesAndSendsNotification() { // Arrange - var userId = 1; - var type = NotificationStatus.QUESTION; - var resourceId = 100; - var message = "Test notification message"; - var actorUser = new ApplicationUser - { - Id = 2, - UserName = "test_actor", - AvatarPath = "actor.jpg" - }; - - // Setup specific mock behavior for this test - _mockClients.Setup(p => p.Group($"user_{userId}")).Returns(_mockClientProxy.Object); + int userId = 1; + var type = NotificationStatus.FOLLOW; + int resourceId = 100; + string message = "Test notification"; + var actorUser = new ApplicationUser { Id = 2, UserName = "test_user", AvatarPath = "test.jpg" }; - _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny())) - .Returns(Task.CompletedTask); + _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny())); _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); - - _notificationRepositoryMock.Setup(r => r.GetActorUserByResourceId(resourceId, type)) + _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(resourceId, type)) .ReturnsAsync(actorUser); // Act await _notificationService.CreateNotification(userId, type, resourceId, message); // Assert - // Verify notification was added to database - _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.Is(n => - n.UserId == userId && - n.Type == type && - n.ResourceId == resourceId && - n.Message == message && - !n.IsRead && - n.CreatedAt != default && - n.UpdatedAt != default - )), Times.Once); - - // Verify database save was called - _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); - - // Verify actor user was fetched - _notificationRepositoryMock.Verify(r => r.GetActorUserByResourceId(resourceId, type), Times.Once); - - // Verify SignalR notification was sent to correct group - _mockClientProxy.Verify(cp => cp.SendCoreAsync( - "ReceiveNotification", - It.Is(args => args.Length == 1 && - ((NotificationDto)args[0]).UserId == userId && - ((NotificationDto)args[0]).Type == type.ToString() && - ((NotificationDto)args[0]).Message == message && - ((NotificationDto)args[0]).Actor.Username == "test_actor" && - ((NotificationDto)args[0]).Actor.AvatarPath == "actor.jpg" - ), - default(CancellationToken) - ), Times.Once); - } - - [Fact] - public async Task CreateNotification_WhenDatabaseSaveFails_ThrowsException() - { - // Arrange - var userId = 1; - var type = NotificationStatus.ANSWER; - var resourceId = 300; - var message = "Answer notification"; - - _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny())) - .Returns(Task.CompletedTask); - _unitOfWorkMock.Setup(p => p.SaveAsync()) - .ThrowsAsync(new Exception("Database error")); - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => _notificationService.CreateNotification(userId, type, resourceId, message) - ); - - Assert.Equal("Database error", exception.Message); - - // Verify notification was attempted to be added _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.IsAny()), Times.Once); _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + _mockClientProxy.Verify(p => p.SendCoreAsync("ReceiveNotification", It.IsAny(), default), Times.Once); } } - - } \ No newline at end of file From e8149361d0662f935fbc9e75dcbf86171d4c767b Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Tue, 26 Aug 2025 02:25:22 +0300 Subject: [PATCH 53/92] Add Default Values to CreatedAt, UpdatedAt and DeletedAt --- AskFm/AskFm.DAL/AppDbContext.cs | 9 +- ...0825225059_AddCreatedAtDefault.Designer.cs | 779 +++++++++++++++++ .../20250825225059_AddCreatedAtDefault.cs | 163 ++++ ...ddUpdatedAtAndDeletedAtDefault.Designer.cs | 811 ++++++++++++++++++ ...5230515_AddUpdatedAtAndDeletedAtDefault.cs | 307 +++++++ .../Migrations/AppDbContextModelSnapshot.cs | 96 ++- 6 files changed, 2138 insertions(+), 27 deletions(-) create mode 100644 AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs diff --git a/AskFm/AskFm.DAL/AppDbContext.cs b/AskFm/AskFm.DAL/AppDbContext.cs index 4f35d3d..7c9196c 100644 --- a/AskFm/AskFm.DAL/AppDbContext.cs +++ b/AskFm/AskFm.DAL/AppDbContext.cs @@ -32,13 +32,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entityType.ClrType).Property("DeletedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("UpdatedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("CreatedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("IsDeleted") .HasColumnType("BIT") diff --git a/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs new file mode 100644 index 0000000..66961a8 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs @@ -0,0 +1,779 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250825225059_AddCreatedAtDefault")] + partial class AddCreatedAtDefault + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs new file mode 100644 index 0000000..475bfac --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs @@ -0,0 +1,163 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class AddCreatedAtDefault : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs new file mode 100644 index 0000000..443269e --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs @@ -0,0 +1,811 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250825230515_AddUpdatedAtAndDeletedAtDefault")] + partial class AddUpdatedAtAndDeletedAtDefault + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs new file mode 100644 index 0000000..5e8c59e --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs @@ -0,0 +1,307 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class AddUpdatedAtAndDeletedAtDefault : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index 796abaa..832c22c 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -50,10 +50,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("Email") .IsRequired() @@ -113,7 +117,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserName") .IsRequired() @@ -150,10 +156,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(1000)"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -170,7 +180,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserId") .HasColumnType("int"); @@ -195,10 +207,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -206,7 +222,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("UserId", "CommentId"); @@ -224,10 +242,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsActive") .ValueGeneratedOnAdd() @@ -240,7 +262,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("FollowerId", "FollowedId"); @@ -258,10 +282,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -272,7 +300,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserId") .HasColumnType("int"); @@ -300,10 +330,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -311,7 +345,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("SavedThreadId", "UserId"); @@ -340,10 +376,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -360,7 +400,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("isAnonymous") .HasColumnType("bit"); @@ -383,10 +425,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -394,7 +440,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("ThreadId", "UserId"); From e8d5ceaeda490ef9be9e6608addbf173fd616bdd Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Wed, 27 Aug 2025 21:39:18 +0300 Subject: [PATCH 54/92] refactor: edited the current user implementation on UserService and the mapping on 'profile' endpoint --- AskFm/AskFm.API/Controllers/UserController.cs | 15 ++++++++++++--- .../Services/UserIdentityService/IUserService.cs | 3 ++- .../Services/UserIdentityService/UserService.cs | 14 +++----------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 87a3a97..381dc74 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -37,16 +37,25 @@ public async Task GetUsers() } [HttpGet] - [Route("current-user")] + [Route("profile")] public async Task GetCurrentUserAsync() { var result = await _userService.GetCurrentUserAsync(); - + if (!result.success) { return BadRequest(result.Errors); } - return Ok(result.Data); + ReadUserDTO readUserDTO = new ReadUserDTO() + { + Name = result.Data.Name, + Email = result.Data.Email, + AvatarPath = result.Data.AvatarPath, + Bio = result.Data.Bio, + followerCount = result.Data.FollowersCount, + LastSeen = result.Data.LastSeen, + }; + return Ok(readUserDTO); } /* GET Users only for now diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs index 51a9e21..93b7796 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -1,4 +1,5 @@ using AskFm.BLL.DTO.UserDTOs; +using AskFm.DAL.Models; namespace AskFm.BLL.Services.UserIdentityService; @@ -10,7 +11,7 @@ public interface IUserService Task> UnfollowUserAsync(int followerId, int targetUserId); Task UpdateLastSeenAsync(int userId, DateTime lastSeen); Task> GetUserByIdAsync(int userId); - Task> GetCurrentUserAsync(); + Task> GetCurrentUserAsync(); Task> ResetPassword(string newPassword); Task> ConfirmEmail(); diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 9169703..8decd39 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -52,7 +52,7 @@ public Task> GetUserByIdAsync(int userId) throw new NotImplementedException(); } - public async Task> GetCurrentUserAsync() + public async Task> GetCurrentUserAsync() { string email = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.Email).Value; if (string.IsNullOrEmpty(email)) @@ -61,19 +61,11 @@ public async Task> GetCurrentUserAsync() { "Can't Access Current user" }; - return await ServiceResult.Failure(errors); + return await ServiceResult.Failure(errors); } var currentAppUser = await _userManager.FindByEmailAsync(email); - return await ServiceResult.Success(new ReadUserDTO() - { - Name = currentAppUser.Name, - Email = currentAppUser.Email, - LastSeen = currentAppUser.LastSeen, - Bio = currentAppUser.Bio, - AvatarPath = currentAppUser.AvatarPath, - followerCount = currentAppUser.FollowersCount - }); + return await ServiceResult.Success(currentAppUser); } public Task> ResetPassword(string newPassword) From 81d4655b50ff8f38a2f6a9db9d3ccffaeebb1d2a Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Wed, 27 Aug 2025 18:56:41 +0300 Subject: [PATCH 55/92] enabled swaggerUI --- AskFm/AskFm.API/Properties/launchSettings.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AskFm/AskFm.API/Properties/launchSettings.json b/AskFm/AskFm.API/Properties/launchSettings.json index dc599be..280f933 100644 --- a/AskFm/AskFm.API/Properties/launchSettings.json +++ b/AskFm/AskFm.API/Properties/launchSettings.json @@ -4,7 +4,8 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, + "launchUrl": "swagger", "applicationUrl": "http://localhost:5180", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -13,7 +14,8 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, + "launchUrl": "swagger", "applicationUrl": "https://localhost:7115;http://localhost:5180", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" From de36d6fb2261d250f4ea63482c79eb606175cf2e Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Thu, 28 Aug 2025 15:43:03 +0300 Subject: [PATCH 56/92] Refactor: - Deleted CreateCommentLikeDto , no need for it after using the GetCurrentUser method - Marked CommentsController as Authorize --- AskFm/AskFm.API/Controllers/CommentController.cs | 14 ++++++++++---- AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs | 8 -------- 2 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs diff --git a/AskFm/AskFm.API/Controllers/CommentController.cs b/AskFm/AskFm.API/Controllers/CommentController.cs index a6a0fef..949f3f6 100644 --- a/AskFm/AskFm.API/Controllers/CommentController.cs +++ b/AskFm/AskFm.API/Controllers/CommentController.cs @@ -1,29 +1,35 @@ using AskFm.BLL.DTO; using AskFm.BLL.Services; +using AskFm.BLL.Services.UserIdentityService; using AskFm.DAL.Interfaces; using AutoMapper; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace AskFm.API.Controllers; [ApiController] -[Route("[controller]")] +[Route("api/[controller]")] +[Authorize(AuthenticationSchemes = "Bearer")] public class CommentController : ControllerBase { private readonly ICommentLikeService _commentLikeService; private readonly ICommentService _commentService; private readonly ILogger _logger; + private readonly IUserService _userService; public CommentController( ICommentLikeService commentLikeService, ICommentService commentService, + IUserService userService, ILogger logger) { _commentLikeService = commentLikeService; _logger = logger; _commentService = commentService; + _userService = userService; } @@ -56,13 +62,13 @@ public async Task GetAllLikes(int id) // POST api/comment/{id}/likes -> add a like for a Comment with id = id [HttpPost("{id}/likes")] - public async Task AddLike(int id, [FromBody] CreateCommentLikeDto likeDto) + public async Task AddLike(int id) { try { - var userId = likeDto.UserId; + var user = await _userService.GetCurrentUserAsync(); - var createdLike = await _commentLikeService.AddLikeAsync(id, userId); + var createdLike = await _commentLikeService.AddLikeAsync(id, user.Data.Id); return CreatedAtAction( nameof(GetAllLikes), diff --git a/AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs b/AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs deleted file mode 100644 index 3fdc72d..0000000 --- a/AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AskFm.BLL.DTO; - -public class CreateCommentLikeDto -{ - public int CommentId; - public int UserId; - public DateTime CreatedAt; -} \ No newline at end of file From fcce8fd56991c5d1a12ed6b212373a35e2d5da26 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Thu, 28 Aug 2025 19:18:40 +0300 Subject: [PATCH 57/92] refactor: - Applied the ServiceResult on CommentService and CommentLikeService --- AskFm/AskFm.API/Controllers/AuthController.cs | 8 +- .../Controllers/CommentController.cs | 48 ++++++-- .../AskFm.BLL/Services/CommentLikeService.cs | 103 +++++++++++++----- AskFm/AskFm.BLL/Services/CommentService.cs | 13 ++- .../AskFm.BLL/Services/ICommentLikeService.cs | 6 +- AskFm/AskFm.BLL/Services/ICommentService.cs | 2 +- AskFm/Tests/CommentLikeServiceTest.cs | 4 +- 7 files changed, 134 insertions(+), 50 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/AuthController.cs b/AskFm/AskFm.API/Controllers/AuthController.cs index 58b635d..32d199f 100644 --- a/AskFm/AskFm.API/Controllers/AuthController.cs +++ b/AskFm/AskFm.API/Controllers/AuthController.cs @@ -30,7 +30,7 @@ public async Task RegisterUser(RegisterUserDTO registerUser) ServiceResult result = await _authService.RegisterAsync(registerUser); if (!result.success) { - return BadRequest(result); + return BadRequest(result.Errors); } setRefreshToken(result.Data.RefreshToken.Token,result.Data.RefreshToken.ExpireOn); return Ok(result); @@ -47,7 +47,7 @@ public async Task Login(LoginDTO login) ServiceResult result = await _authService.LoginAsync(login); if (!result.success) { - return BadRequest(result); + return BadRequest(result.Errors); } if (!string.IsNullOrEmpty(result.Data.Token)) { @@ -70,7 +70,7 @@ public async Task RefreshToken(int id) if (!result.success) { - return BadRequest(result); + return BadRequest(result.Errors); } return Ok(result); @@ -91,7 +91,7 @@ public async Task Logout(int id) if (!result.success) { - return BadRequest(result); + return BadRequest(result.Errors); } diff --git a/AskFm/AskFm.API/Controllers/CommentController.cs b/AskFm/AskFm.API/Controllers/CommentController.cs index 949f3f6..21887c9 100644 --- a/AskFm/AskFm.API/Controllers/CommentController.cs +++ b/AskFm/AskFm.API/Controllers/CommentController.cs @@ -43,12 +43,20 @@ public async Task GetAllLikes(int id) try { var likes = await _commentLikeService.GetLikesForCommentAsync(id); + if (!likes.success) + { + return BadRequest(likes.Errors); + } return Ok(likes); } catch (ArgumentException ex) { _logger.LogWarning(ex, "Comment not found with id: {CommentId}", id); - return NotFound(new { message = ex.Message }); + return NotFound(new + { + message = ex.Message, + + }); } catch (Exception ex) { @@ -68,12 +76,21 @@ public async Task AddLike(int id) { var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + var createdLike = await _commentLikeService.AddLikeAsync(id, user.Data.Id); + if (!createdLike.success) + { + return BadRequest(createdLike.Errors); + } return CreatedAtAction( nameof(GetAllLikes), new { id = id }, - createdLike); + createdLike.Data); } catch (ArgumentException ex) { @@ -96,18 +113,31 @@ public async Task AddLike(int id) [HttpDelete("{id}/likes")] - public async Task DeleteLike(int id, int userId) + public async Task DeleteLike(int id) { + int userId = 0; try { - var comment = _commentService.GetComment(id); - if (comment == null) - throw new ArgumentException($"Comment with id {id} not found"); - if (userId <= 0) - return BadRequest(new { message = "Invalid user id." }); + var user = await _userService.GetCurrentUserAsync(); + + if (user==null || !user.success) + return BadRequest(user.Errors); + - await _commentLikeService.DeleteLikeAsync(id, userId); + userId = user.Data.Id; + var comment = await _commentService.GetCommentAsync(id); + + if (comment == null || !user.success) + return BadRequest(user.Errors); + + + var result = await _commentLikeService.DeleteLikeAsync(id, userId); + + if (!result.success) + { + return BadRequest(result.Errors); + } return NoContent(); } diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs index b238aee..39a68c5 100644 --- a/AskFm/AskFm.BLL/Services/CommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -1,8 +1,10 @@ +using System.Runtime.InteropServices.JavaScript; using AskFm.BLL.DTO; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; using AutoMapper; using Microsoft.Extensions.Logging; +using Microsoft.VisualBasic; namespace AskFm.BLL.Services; @@ -20,7 +22,7 @@ public CommentLikeService(IUnitOfWork unitOfWork, ILogger l - public async Task> GetLikesForCommentAsync(int commentId) + public async Task>> GetLikesForCommentAsync(int commentId) { try { @@ -44,16 +46,16 @@ public async Task> GetLikesForCommentAsync(int comme UserName = like.User?.UserName }); - return likeDtos; + return await ServiceResult>.Success(likeDtos); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving likes for comment id: {CommentId}", commentId); - throw; + return await ServiceResult>.Failure(new List() { ex.Message }); } } - public async Task AddLikeAsync(int commentId, int userId) + public async Task> AddLikeAsync(int commentId, int userId) { try { @@ -61,19 +63,60 @@ public async Task AddLikeAsync(int commentId, int userId) commentId, userId); var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); + var user = await _unitOfWork.Users.GetByIdAsync(userId); + if (user == null) + { + var errors = new List() + { + $"User with id {userId} not found" + }; + return await ServiceResult.Failure(errors); + } + string userName = user.UserName; if (comment == null) { - throw new ArgumentException($"Comment with id {commentId} not found"); + var errors = new List() + { + $"Comment with id {commentId} not found" + }; + return await ServiceResult.Failure(errors); } var existingLike = await _unitOfWork.CommentLikes.FindAsync( - cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted + cl => cl.CommentId == commentId && cl.UserId == userId ); + // if the CommentLike already exit in the DB if (existingLike != null) { - throw new InvalidOperationException("User has already liked this comment"); + // if the The use already liked this comment + if (!existingLike.IsDeleted) + { + var errors = new List() + { + "User has already liked this comment" + }; + return await ServiceResult.Failure(errors); + } + + // otherwise , the user liked the commend , then unliked it , and then wants to like it again + existingLike.IsDeleted = false; + comment.LikeCount++; + _unitOfWork.Comments.Update(comment); + await _unitOfWork.SaveAsync(); + _logger.LogInformation("Like added successfully for comment id: {CommentId}", commentId); + + + // updating the createdAt column to Now , ignoring the first time the user liked the comment + existingLike.CreatedAt = DateTime.Now; + return await ServiceResult.Success(new CommentLikeDto + { + CommentId = existingLike.CommentId, + UserId = existingLike.UserId, + UserName = userName, + CreatedAt = existingLike.CreatedAt + }); } var newLike = new CommentLike @@ -90,44 +133,40 @@ public async Task AddLikeAsync(int commentId, int userId) await _unitOfWork.SaveAsync(); _logger.LogInformation("Like added successfully for comment id: {CommentId}", commentId); - - - var user = await _unitOfWork.Users.GetByIdAsync(userId); - - if (user == null) - throw new ArgumentException($"User with id {userId} not found"); - - string userName = user.UserName; - - - return new CommentLikeDto + return await ServiceResult.Success(new CommentLikeDto { CommentId = newLike.CommentId, UserId = newLike.UserId, UserName = userName, CreatedAt = newLike.CreatedAt - }; + }); } catch (Exception ex) { _logger.LogError(ex, "Error adding like for comment id: {CommentId} by user id: {UserId}", commentId, userId); - throw; + return await ServiceResult.Failure(new List(){ex.Message}); } } - public async Task DeleteLikeAsync(int commentId, int userId) + public async Task> DeleteLikeAsync(int commentId, int userId) { try { _logger.LogInformation("Deleting the comment like from user {userid} on comment id {commentId}", userId, commentId); var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); - - if(comment == null) - throw new ArgumentException($"User didn't like this comment"); + + if (comment == null) + { + var errors = new List() + { + "$User didn't like this comment" + }; + return await ServiceResult.Failure(errors); + } var commentLike = await @@ -135,8 +174,15 @@ public async Task DeleteLikeAsync(int commentId, int userId) predicate: cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted); // if the user didn't like this comment before - if(commentLike == null) - throw new ArgumentException($"User didn't like this comment"); + if (commentLike == null) + { + var errors = new List() + { + "User didn't like this comment" + + }; + return await ServiceResult.Failure(errors); + } await _unitOfWork.CommentLikes.RemoveAsync(commentLike); @@ -150,12 +196,13 @@ public async Task DeleteLikeAsync(int commentId, int userId) _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); - return true; + + return await ServiceResult.Success(); } catch (Exception e) { _logger.LogError(e, "Failed to delete like by user {UserId} on comment {CommentId}", userId, commentId); - throw; + return await ServiceResult.Failure(new List(){e.Message}); } } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/CommentService.cs b/AskFm/AskFm.BLL/Services/CommentService.cs index 8cec928..81f7c98 100644 --- a/AskFm/AskFm.BLL/Services/CommentService.cs +++ b/AskFm/AskFm.BLL/Services/CommentService.cs @@ -11,10 +11,17 @@ public CommentService(IUnitOfWork unitOfWork) _unitOfWork = unitOfWork; } - public Comment GetComment(int commentId) + public async Task> GetCommentAsync(int commentId) { - var comment = _unitOfWork.Comments.GetById(commentId); - return comment; + try + { + var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); + return await ServiceResult.Success(comment); + } + catch (Exception e) + { + return await ServiceResult.Failure(new List(){e.Message}); + } } diff --git a/AskFm/AskFm.BLL/Services/ICommentLikeService.cs b/AskFm/AskFm.BLL/Services/ICommentLikeService.cs index d2496bd..5285cd5 100644 --- a/AskFm/AskFm.BLL/Services/ICommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/ICommentLikeService.cs @@ -4,7 +4,7 @@ namespace AskFm.BLL.Services; public interface ICommentLikeService { - Task> GetLikesForCommentAsync(int commentId); - Task AddLikeAsync(int commentId, int userId); - Task DeleteLikeAsync(int commentId, int userId); + Task>> GetLikesForCommentAsync(int commentId); + Task> AddLikeAsync(int commentId, int userId); + Task> DeleteLikeAsync(int commentId, int userId); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ICommentService.cs b/AskFm/AskFm.BLL/Services/ICommentService.cs index 1a3e213..dda8314 100644 --- a/AskFm/AskFm.BLL/Services/ICommentService.cs +++ b/AskFm/AskFm.BLL/Services/ICommentService.cs @@ -4,5 +4,5 @@ namespace AskFm.BLL.Services; public interface ICommentService { - Comment GetComment(int commentId); + Task> GetCommentAsync(int commentId); } \ No newline at end of file diff --git a/AskFm/Tests/CommentLikeServiceTest.cs b/AskFm/Tests/CommentLikeServiceTest.cs index e5f8de8..3d1f7e5 100644 --- a/AskFm/Tests/CommentLikeServiceTest.cs +++ b/AskFm/Tests/CommentLikeServiceTest.cs @@ -191,7 +191,7 @@ public async Task GetLikesForComment_WhenCommentLikesIsNotEmpty_ReturnsCommentLi // Assert Assert.NotNull(result); - Assert.Equal(5, result.Count()); + Assert.Equal(5, result.Data.Count()); Assert.Equivalent(expectedDtos, result); @@ -227,7 +227,7 @@ public async Task GetLikesForComment_WhenCommentLikesIsEmpty_ReturnsEmptyList() // Assert - Assert.Empty(result); + Assert.Empty(result.Data); } From 462e88c4312ee269268e5d9dd772ab5142fc749f Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Thu, 28 Aug 2025 19:42:26 +0300 Subject: [PATCH 58/92] test: Fixed some fail tests --- AskFm/Tests/CommentLikeServiceTest.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/AskFm/Tests/CommentLikeServiceTest.cs b/AskFm/Tests/CommentLikeServiceTest.cs index 3d1f7e5..266de4c 100644 --- a/AskFm/Tests/CommentLikeServiceTest.cs +++ b/AskFm/Tests/CommentLikeServiceTest.cs @@ -77,12 +77,20 @@ public async Task AddLike_WhenCommentNotFoundOrDeleted_ThrowException() // Arrange var commentId = 1000; var userId = 5; + var user = new ApplicationUser() + { + Name = "ziad" + }; _mockCommentRepository.Setup(repo => repo.GetByIdAsync(commentId)) .ReturnsAsync((Comment)null); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(userId)) + .ReturnsAsync(user); - // Assert - await Assert.ThrowsAsync(() => _commentLikeService.AddLikeAsync(commentId, userId)); + var result = await _commentLikeService.AddLikeAsync(commentId, userId); + // Assert + Assert.False(result.success); + Assert.Contains($"Comment with id {commentId} not found", result.Errors); _mockCommentLikeRepository.Verify(repo => repo.AddAsync(It.IsAny()), Times.Never); _mockUnitOfWork.Verify(uow => uow.SaveAsync(), Times.Never); @@ -190,10 +198,11 @@ public async Task GetLikesForComment_WhenCommentLikesIsNotEmpty_ReturnsCommentLi // Assert - Assert.NotNull(result); + Assert.True(result.success); + Assert.NotNull(result.Data); Assert.Equal(5, result.Data.Count()); - Assert.Equivalent(expectedDtos, result); + Assert.Equivalent(expectedDtos, result.Data); } From b430c4e87113ddc639caf3fa3eb5f0f65a5f1853 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Fri, 29 Aug 2025 22:04:59 +0300 Subject: [PATCH 59/92] User Endpoints - Update User - Delete User - Follow User - UnFollow User - Update Password Tests: - Update User Tests TODO: - reset Password - reset Email - Decide how deleted endpoint will behave with relations --- AskFm/AskFm.API/Controllers/AuthController.cs | 8 +- AskFm/AskFm.API/Controllers/UserController.cs | 126 +++++++++- AskFm/AskFm.API/Program.cs | 28 ++- AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs | 1 - .../UserIdentityService/AuthService.cs | 9 - .../UserIdentityService/IAuthService.cs | 2 +- .../UserIdentityService/IUserService.cs | 7 +- .../UserIdentityService/UserService.cs | 216 ++++++++++++++++-- AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs | 4 +- .../AskFm.DAL/Repositories/UserRepository.cs | 1 + AskFm/AskFm.DAL/UnitOfWork.cs | 7 +- AskFm/Tests/Tests.csproj | 7 + AskFm/Tests/UserTests.cs | 138 +++++++++++ 13 files changed, 510 insertions(+), 44 deletions(-) create mode 100644 AskFm/Tests/UserTests.cs diff --git a/AskFm/AskFm.API/Controllers/AuthController.cs b/AskFm/AskFm.API/Controllers/AuthController.cs index 58b635d..32d199f 100644 --- a/AskFm/AskFm.API/Controllers/AuthController.cs +++ b/AskFm/AskFm.API/Controllers/AuthController.cs @@ -30,7 +30,7 @@ public async Task RegisterUser(RegisterUserDTO registerUser) ServiceResult result = await _authService.RegisterAsync(registerUser); if (!result.success) { - return BadRequest(result); + return BadRequest(result.Errors); } setRefreshToken(result.Data.RefreshToken.Token,result.Data.RefreshToken.ExpireOn); return Ok(result); @@ -47,7 +47,7 @@ public async Task Login(LoginDTO login) ServiceResult result = await _authService.LoginAsync(login); if (!result.success) { - return BadRequest(result); + return BadRequest(result.Errors); } if (!string.IsNullOrEmpty(result.Data.Token)) { @@ -70,7 +70,7 @@ public async Task RefreshToken(int id) if (!result.success) { - return BadRequest(result); + return BadRequest(result.Errors); } return Ok(result); @@ -91,7 +91,7 @@ public async Task Logout(int id) if (!result.success) { - return BadRequest(result); + return BadRequest(result.Errors); } diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 381dc74..ea58a39 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -23,7 +23,7 @@ public UserController(IUnitOfWork unitOfWork, IAuthService authService, IUserSer _userService = userService; } [HttpGet] - [Route("GetUsers")] + [Route("GetAllUsers")] public async Task GetUsers() { var result = _unitOfWork.Users.GetAll().Select(u => new @@ -57,16 +57,124 @@ public async Task GetCurrentUserAsync() }; return Ok(readUserDTO); } + + [HttpGet] + [Route("profile/{userId}")] + public async Task GetUserAsync(int userId) + { + var result = await _userService.GetUserByIdAsync(userId); + if (!result.success) + { + return BadRequest(result.Errors); + } + return Ok(result.Data); + } + + [HttpPost] + [Route("profile/update/{userId}")] + public async Task UpdateUserAsync(int userId, UpdateUserDTO updatedUser) + { + if (!await _checkCurrentUser(userId)) + { + return Forbid("Cannot Update this user"); + } + var result = await _userService.UpdateUserAsync(userId, updatedUser); + if (!result.success) + { + return BadRequest(result.Errors); + } + return RedirectToAction("GetUserAsync", new { userId = userId }); + } + + + [HttpDelete] + [Route("profile/{userId}")] + public async Task DeleteUserAsync(int userId) + { + if (!await _checkCurrentUser(userId)) + { + return Forbid("Cannot Remove this user"); + } + var result = await _userService.DeleteUserAsync(userId); + if (!result.success) + { + return BadRequest(result.Errors); + } + return Ok(); + } + + + [HttpPost] + [Route("profile/{followerId}/follow/{targetUserId}")] + public async Task FollowUserAsync(int followerId, int targetUserId) + { + if (await _checkCurrentUser(targetUserId)) + { + return Forbid("Cannot Follow the current user"); + } + if (!await _checkCurrentUser(followerId)) + { + return Forbid("User can't perform this follow"); + } + + var result = await _userService.FollowUserAsync(followerId, targetUserId); + if (!result.success) + { + return BadRequest(result.Errors); + } + return Ok(); + } + + [HttpPost] + [Route("profile/{followerId}/unfollow/{targetUserId}")] + public async Task UnFollowUserAsync(int followerId, int targetUserId) + { + if (await _checkCurrentUser(targetUserId)) + { + return Forbid("Cannot unFollow the current user"); + } + if (!await _checkCurrentUser(followerId)) + { + return Forbid("User can't perform this unfollow"); + } + + var result = await _userService.UnfollowUserAsync(followerId, targetUserId); + if (!result.success) + { + return BadRequest(result.Errors); + } + return Ok(); + } + + [HttpPost] + [Route("profile/update/pass/{userId}")] + public async Task UpdatePassword(int userId, string currentPassword, string updatedPassword) + { + if (await _checkCurrentUser(userId)) + { + return Forbid("Cannot unFollow the current user"); + } + var result = await _userService.UpdatePassword(userId, currentPassword, updatedPassword); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(); + } + + // ------------------------------------------------------------------- + // Helper functions + private async Task _checkCurrentUser(int userId) + { + var current_user = _userService.GetCurrentUserAsync(); + return current_user.Id != userId; + } + + /* - GET Users only for now - getUserbyId - EditUser - DeleteUser - FollowUser - unfollowUser - reset password + update email confirm email - Helper Function: getCurrentUserId */ } \ No newline at end of file diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index bf1d8f9..6aeb748 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -46,7 +46,33 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); + builder.Services.AddSwaggerGen(setup => + { + // Include 'SecurityScheme' to use JWT Authentication + var jwtSecurityScheme = new OpenApiSecurityScheme + { + BearerFormat = "JWT", + Name = "JWT Authentication", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = JwtBearerDefaults.AuthenticationScheme, + Description = "Put **_ONLY_** your JWT Bearer token on textbox below!", + + Reference = new OpenApiReference + { + Id = JwtBearerDefaults.AuthenticationScheme, + Type = ReferenceType.SecurityScheme + } + }; + + setup.AddSecurityDefinition(jwtSecurityScheme.Reference.Id, jwtSecurityScheme); + + setup.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { jwtSecurityScheme, Array.Empty() } + }); + + }); JwtOptions jwtOptions = new JwtOptions diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs index 3f13b89..6e44895 100644 --- a/AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs @@ -3,7 +3,6 @@ namespace AskFm.BLL.DTO.UserDTOs; public class UpdateUserDTO { public string Name { get; set; } - public string Email { get; set; } public string Bio { get; set; } public string AvatarPath { get; set; } diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs index 1bbcc0e..98e0170 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs @@ -168,18 +168,9 @@ public async Task> RevokeRefreshTokenAsync(int id, string re oldRefreshToken.RevokedOn = DateTime.UtcNow; await _userManager.UpdateAsync(user); - return await ServiceResult.Success(true); } - - public void Logout() - { - - - } - - private async Task> GetAuthToken(ApplicationUser user) { var token = await GenerateJwtToken(user); diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs index 249eb83..3e6fa04 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs @@ -6,8 +6,8 @@ namespace AskFm.BLL.Services.UserIdentityService; public interface IAuthService { public Task> LoginAsync(LoginDTO request); + // public Task> ResetPasswordAsync(string Email); public Task> RegisterAsync(RegisterUserDTO request); Task> RefreshTokenAsync(int id, string refreshToken); public Task> RevokeRefreshTokenAsync(int id, string refreshToken); - public void Logout(); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs index 93b7796..f2aad1a 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -5,14 +5,15 @@ namespace AskFm.BLL.Services.UserIdentityService; public interface IUserService { - Task> UpdateUserAsync(int userId, UpdateUserDTO updatedUser); + Task> UpdateUserAsync(int userId, UpdateUserDTO updatedUser); Task> DeleteUserAsync(int userId); Task> FollowUserAsync(int followerId, int targetUserId); Task> UnfollowUserAsync(int followerId, int targetUserId); - Task UpdateLastSeenAsync(int userId, DateTime lastSeen); + Task> UpdateLastSeenAsync(int userId); Task> GetUserByIdAsync(int userId); Task> GetCurrentUserAsync(); - Task> ResetPassword(string newPassword); + Task> UpdatePassword(int userId, string currentPassword, string updatedPassword); + Task> ResetEmail(int userId, string updatedEmail); Task> ConfirmEmail(); diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 8decd39..5119e8d 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -4,6 +4,7 @@ using AskFm.DAL.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; namespace AskFm.BLL.Services.UserIdentityService; @@ -22,34 +23,175 @@ public UserService(IUnitOfWork unitOfWork, UserManager userMana } - public Task> UpdateUserAsync(int userId, UpdateUserDTO updatedUser) + public async Task> UpdateUserAsync(int userId, UpdateUserDTO updatedUser) { - throw new NotImplementedException(); + var res = await CheckNullObjectAsync(updatedUser); + if (!res.success) return res; + + var AppUserToUpdate = await _unitOfWork.Users.GetByIdAsync(userId); + if (AppUserToUpdate == null) + { + return await ServiceResult.Failure(new List { "User not found" }); + } + AppUserToUpdate.Name = updatedUser.Name; + AppUserToUpdate.Bio = updatedUser.Bio; + AppUserToUpdate.AvatarPath = updatedUser.AvatarPath; + + await _unitOfWork.Users.UpdateAsync(AppUserToUpdate); + await _unitOfWork.SaveAsync(); + return await ServiceResult.Success(); } - public Task> DeleteUserAsync(int userId) + public async Task> DeleteUserAsync(int userId) { - throw new NotImplementedException(); + var appUser = _unitOfWork.Users.GetById(userId); + var res = await CheckNullObjectAsync(appUser); + if (!res.success) return res; + + await _unitOfWork.Users.RemoveAsync(appUser); + await _unitOfWork.SaveAsync(); + return await ServiceResult.Success(true); } - public Task> FollowUserAsync(int followerId, int targetUserId) + public async Task> FollowUserAsync(int followerId, int targetUserId) { - throw new NotImplementedException(); + + if (followerId == targetUserId) + { + return await ServiceResult.Failure(new List { "Invalid user" }); + } + + await using var transaction = await _unitOfWork.BeginTransactionAsync(); + try + { + var userFollower = await _unitOfWork.Users.GetByIdAsync(followerId); + var res = await CheckNullObjectAsync(userFollower); + if (!res.success) return res; + + var targetUser = await _unitOfWork.Users.GetByIdAsync(targetUserId); + var res2 = await CheckNullObjectAsync(targetUser); + if (!res2.success) return res; + + var followExist = await _unitOfWork.Follows.GetAll() + .FirstOrDefaultAsync(f => f.FollowedId == targetUserId + && f.FollowerId == followerId); + + if (followExist == null) + { + Follow follow = new Follow() + { + FollowerId = followerId, + FollowedId = targetUserId, + + }; + + userFollower.FollowingCount++; + targetUser.FollowersCount++; + await _unitOfWork.Follows.AddAsync(follow); + + } + else + { + followExist.IsActive = true; + if (followExist.IsDeleted) + { + userFollower.FollowingCount++; + targetUser.FollowersCount++; + followExist.IsDeleted = false; + } + + await _unitOfWork.Follows.UpdateAsync(followExist); + } + + await _unitOfWork.Users.UpdateAsync(userFollower); + await _unitOfWork.Users.UpdateAsync(targetUser); + await _unitOfWork.SaveAsync(); + await transaction.CommitAsync(); + return await ServiceResult.Success(true); + } + catch (Exception) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Invalid Follow Operation" }); + } } - public Task> UnfollowUserAsync(int followerId, int targetUserId) + public async Task> UnfollowUserAsync(int followerId, int targetUserId) { - throw new NotImplementedException(); + if (followerId == targetUserId) + { + return await ServiceResult.Failure(new List { "Invalid user" }); + } + await using var transaction = await _unitOfWork.BeginTransactionAsync(); + try + { + var userFollower = await _unitOfWork.Users.GetByIdAsync(followerId); + var res = await CheckNullObjectAsync(userFollower); + if (!res.success) return res; + + var targetUser = await _unitOfWork.Users.GetByIdAsync(targetUserId); + var res2 = await CheckNullObjectAsync(targetUser); + if (!res2.success) return res; + + + var followExist = await _unitOfWork.Follows.GetAll() + .FirstOrDefaultAsync(f => f.FollowedId == targetUserId + && f.FollowerId == followerId && !f.IsDeleted); + + if (followExist != null) + { + followExist.IsDeleted = true; + await _unitOfWork.SaveAsync(); + + } + + transaction.Commit(); + + return await ServiceResult.Success(true); + } + catch (Exception) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Invalid unfollow operation" }); + } } - public Task UpdateLastSeenAsync(int userId, DateTime lastSeen) + public async Task> UpdateLastSeenAsync(int userId) { - throw new NotImplementedException(); + var appUser =await _unitOfWork.Users.GetByIdAsync(userId); + var res = await CheckNullObjectAsync(appUser); + if (!res.success) return res; + await using var transaction = await _unitOfWork.BeginTransactionAsync(); + try + { + appUser.LastSeen = DateTime.Now; + await _unitOfWork.Users.UpdateAsync(appUser); + await transaction.CommitAsync(); + return await ServiceResult.Success(true); + } + catch (Exception) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Invalid update operation" }); + } + } - public Task> GetUserByIdAsync(int userId) + public async Task> GetUserByIdAsync(int userId) { - throw new NotImplementedException(); + var user = _unitOfWork.Users.GetById(userId); + var res = await CheckNullObjectAsync(user); + if (!res.success) return res; + + return await ServiceResult.Success(new ReadUserDTO() + { + Name = user.Name, + Email = user.Email, + LastSeen = user.LastSeen, + Bio = user.Bio, + AvatarPath = user.AvatarPath, + followerCount = user.FollowersCount + }); } public async Task> GetCurrentUserAsync() @@ -68,13 +210,61 @@ public async Task> GetCurrentUserAsync() return await ServiceResult.Success(currentAppUser); } - public Task> ResetPassword(string newPassword) + public async Task> UpdatePassword(int userId, string currentPassword, string updatedPassword) { + var appUser = await _unitOfWork.Users.GetByIdAsync(userId); + var res = await CheckNullObjectAsync(appUser); + if (!res.success) return res; + + var passwordValid = await _userManager.CheckPasswordAsync(appUser, updatedPassword); + if (!passwordValid) + { + var errors = new List { "Invalid Password." }; + return await ServiceResult.Failure(errors); + } + if (currentPassword==updatedPassword) + { + var errors = new List { "It is the same old password." }; + return await ServiceResult.Failure(errors); + } + + var result = await _userManager.ChangePasswordAsync(appUser, currentPassword, updatedPassword); + if (!result.Succeeded) + { + var errors = new List { "Cannot Update Current Password." }; + return await ServiceResult.Failure(errors); + } + await _userManager.UpdateAsync(appUser); + return await ServiceResult.Success(true); + } + + public async Task> ResetEmail(int userId, string updatedEmail) + { + var userApp =await _unitOfWork.Users.GetByIdAsync(userId); + var res = await CheckNullObjectAsync(userApp); + if(!res.success) return res; + + //var emailResult = _userManager.GenerateChangeEmailTokenAsync(); + + // throw new NotImplementedException(); + } public Task> ConfirmEmail() { throw new NotImplementedException(); } + + + // Helper check null object + private async Task> CheckNullObjectAsync(Y obj, string errorMessage = "Not Found") + { + if (obj == null) + { + return await ServiceResult.Failure(new List() { errorMessage }); + } + + return await ServiceResult.Success(); + } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs index 108f184..c2b6318 100644 --- a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs +++ b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs @@ -1,4 +1,5 @@ using AskFm.DAL.Models; +using Microsoft.EntityFrameworkCore.Storage; using Thread = System.Threading.Thread; namespace AskFm.DAL.Interfaces; @@ -16,5 +17,6 @@ public interface IUnitOfWork : IDisposable int Save(); Task SaveAsync(); - + Task BeginTransactionAsync(); + } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/UserRepository.cs b/AskFm/AskFm.DAL/Repositories/UserRepository.cs index dcf55f3..587003c 100644 --- a/AskFm/AskFm.DAL/Repositories/UserRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/UserRepository.cs @@ -16,4 +16,5 @@ public UserRepository(AppDbContext context) : base(context) .FirstOrDefaultAsync(u => u.UserName == username); } + } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/UnitOfWork.cs b/AskFm/AskFm.DAL/UnitOfWork.cs index 573f3ae..e9dcd85 100644 --- a/AskFm/AskFm.DAL/UnitOfWork.cs +++ b/AskFm/AskFm.DAL/UnitOfWork.cs @@ -1,6 +1,7 @@ using AskFm.DAL.Interfaces; using AskFm.DAL.Models; using AskFm.DAL.Repositories; +using Microsoft.EntityFrameworkCore.Storage; using Thread = System.Threading.Thread; namespace AskFm.DAL; @@ -130,8 +131,10 @@ public int Save() return _context.SaveChanges(); } - public Task SaveAsync() + public async Task SaveAsync() { - return _context.SaveChangesAsync(); + return await _context.SaveChangesAsync(); } + + public async Task BeginTransactionAsync() => await _context.Database.BeginTransactionAsync(); } \ No newline at end of file diff --git a/AskFm/Tests/Tests.csproj b/AskFm/Tests/Tests.csproj index 95d873d..a5b05ce 100644 --- a/AskFm/Tests/Tests.csproj +++ b/AskFm/Tests/Tests.csproj @@ -9,7 +9,9 @@ + + @@ -18,4 +20,9 @@ + + + + + diff --git a/AskFm/Tests/UserTests.cs b/AskFm/Tests/UserTests.cs new file mode 100644 index 0000000..849f524 --- /dev/null +++ b/AskFm/Tests/UserTests.cs @@ -0,0 +1,138 @@ +using AskFm.BLL.DTO.UserDTOs; +using AskFm.BLL.Services.UserIdentityService; +using AskFm.DAL; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; + +namespace Tests; + +public class UserTests +{ + private UserService _userService; + private Mock _mockUnitOfWork; + private Mock _mockHttpContextAccessor; + private Mock> _mockUserManager; + private Mock _mockApplicationUserRepository; + + public UserTests() + { + // mock setup + _mockUserManager = GetMockUserManager(); + _mockHttpContextAccessor = new Mock(); + _mockUnitOfWork = new Mock(); + _mockApplicationUserRepository = new Mock(); + // uesr repo setup + _mockUnitOfWork.Setup(u => u.Users).Returns(_mockApplicationUserRepository.Object); + + // service creation + _userService = new UserService(_mockUnitOfWork.Object, _mockUserManager.Object, _mockHttpContextAccessor.Object); + + // user manager save + _mockUserManager.Setup(um => um.UpdateAsync(It.IsAny())) + .ReturnsAsync(IdentityResult.Success); + + } + + + + /// + /// Test updateUserAsync Service with 3 conditions + /// + [Fact] + public async Task UpdatedUser_UpdatedWithCorrectUserAndCorrectData_SuccessWithUserData() + { + // Arrange + int userId = 1; + var appUser = new ApplicationUser + { + Id = userId, + Name = "OldName", + Bio = "Old Bio", + AvatarPath = "/old.jpg" + }; + + var updatedUser = new UpdateUserDTO + { + Name = "John", + Bio = "this is John, software engineer.", + AvatarPath = "/image.jpg" + }; + + // UnitOfWork + _mockUnitOfWork.Setup(u => u.Users.GetByIdAsync(userId)).ReturnsAsync(appUser); + _mockUnitOfWork.Setup(u => u.Users.UpdateAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockUnitOfWork.Setup(u => u.SaveAsync()).Returns(Task.FromResult(1)); + + + // Act + var result = await _userService.UpdateUserAsync(userId, updatedUser); + + // Assert + Assert.True(result.success); + + _mockUnitOfWork.Verify(u => u.Users.UpdateAsync(It.Is(u => u.Id == userId)), Times.Once); + _mockUnitOfWork.Verify(u => u.SaveAsync(), Times.Once); + } + + [Fact] + public async Task UpdatedUser_ThePassedUserIsNull_UpdateFaild() + { + + } + + [Fact] + public async Task UpdatedUser_ThePassedUserIsNotFound_UpdateFaild() + { + + } + + /// + /// Update DeleteUserAsync with 2 conditions + /// + [Fact] + public async Task DeleteUserAsync_ThePassedUserNotFound_DeleteFaild() + { + + } + [Fact] + public async Task DeleteUserAsync_FoundedUser_DeleteSuccess() + { + + } + + + + + + + // helper functions + private Mock> GetMockUserManager() + { + var store = new Mock>(); + var options = new Mock>(); + var passwordHasher = new Mock>(); + var userValidators = new List>(); + var passwordValidators = new List>(); + var keyNormalizer = new Mock(); + var errors = new Mock(); + var services = new Mock(); + var logger = new Mock>>(); + + return new Mock>( + store.Object, + options.Object, + passwordHasher.Object, + userValidators, + passwordValidators, + keyNormalizer.Object, + errors.Object, + services.Object, + logger.Object); + + } +} \ No newline at end of file From b5277c3bd0f51593027aeaf3881b4899a8298152 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Fri, 29 Aug 2025 23:33:36 +0300 Subject: [PATCH 60/92] Setting refresh token in cookies after refershToken --- AskFm/AskFm.API/Controllers/AuthController.cs | 9 +++------ AskFm/AskFm.API/Controllers/UserController.cs | 9 ++++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/AuthController.cs b/AskFm/AskFm.API/Controllers/AuthController.cs index 32d199f..7eb9d7c 100644 --- a/AskFm/AskFm.API/Controllers/AuthController.cs +++ b/AskFm/AskFm.API/Controllers/AuthController.cs @@ -49,10 +49,8 @@ public async Task Login(LoginDTO login) { return BadRequest(result.Errors); } - if (!string.IsNullOrEmpty(result.Data.Token)) - { - setRefreshToken(result.Data.RefreshToken.Token,result.Data.RefreshToken.ExpireOn); - } + setRefreshToken(result.Data.RefreshToken.Token,result.Data.RefreshToken.ExpireOn); + return Ok(result); } @@ -72,9 +70,8 @@ public async Task RefreshToken(int id) { return BadRequest(result.Errors); } - + setRefreshToken(result.Data.RefreshToken.Token,result.Data.RefreshToken.ExpireOn); return Ok(result); - } [HttpPost("logout/{id}")] diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index ea58a39..7051ed7 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -86,7 +86,6 @@ public async Task UpdateUserAsync(int userId, UpdateUserDTO updat return RedirectToAction("GetUserAsync", new { userId = userId }); } - [HttpDelete] [Route("profile/{userId}")] public async Task DeleteUserAsync(int userId) @@ -96,6 +95,7 @@ public async Task DeleteUserAsync(int userId) return Forbid("Cannot Remove this user"); } var result = await _userService.DeleteUserAsync(userId); + if (!result.success) { return BadRequest(result.Errors); @@ -163,7 +163,8 @@ public async Task UpdatePassword(int userId, string currentPasswo return Ok(); } - // ------------------------------------------------------------------- + + //------------------------------------------------------------------- // Helper functions private async Task _checkCurrentUser(int userId) { @@ -175,6 +176,8 @@ private async Task _checkCurrentUser(int userId) /* update email confirm email - */ + check user not deleted in login + + */ } \ No newline at end of file From 3704164979e603a574784f8ab50a7af61426261b Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Sat, 30 Aug 2025 00:10:00 +0300 Subject: [PATCH 61/92] refactor: Check user is deleted or not in login --- AskFm/AskFm.API/Controllers/UserController.cs | 1 - .../Services/UserIdentityService/AuthService.cs | 11 ++++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 7051ed7..85d32ef 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -103,7 +103,6 @@ public async Task DeleteUserAsync(int userId) return Ok(); } - [HttpPost] [Route("profile/{followerId}/follow/{targetUserId}")] public async Task FollowUserAsync(int followerId, int targetUserId) diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs index 98e0170..3a0b8c7 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs @@ -40,6 +40,7 @@ public async Task> LoginAsync(LoginDTO request) } var getUser = await _userManager.FindByEmailAsync(request.Email); + if (getUser == null) { @@ -50,6 +51,15 @@ public async Task> LoginAsync(LoginDTO request) return await ServiceResult.Failure(erros); } + if (getUser.IsDeleted) + { + var erros = new List + { + "Invalid Email or Password." + }; + return await ServiceResult.Failure(erros); + } + var passwordValid = await _userManager.CheckPasswordAsync(getUser, request.Password); if (!passwordValid) { @@ -62,7 +72,6 @@ public async Task> LoginAsync(LoginDTO request) } - public async Task> RegisterAsync(RegisterUserDTO request) { if (request == null) From 89a363ddd495874c3da0c7d330a4bdb6229f2ddb Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sat, 30 Aug 2025 15:07:34 +0300 Subject: [PATCH 62/92] Add: Added transactions on the CommentLikeService --- .../AskFm.BLL/Services/CommentLikeService.cs | 22 ++++++++++++++++--- AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs | 4 +++- AskFm/AskFm.DAL/UnitOfWork.cs | 7 ++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs index 39a68c5..2033711 100644 --- a/AskFm/AskFm.BLL/Services/CommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -31,7 +31,11 @@ public async Task>> GetLikesForComment var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); if (comment == null) { - throw new ArgumentException($"Comment with id {commentId} not found"); + var errors = new List() + { + $"Comment with id {commentId} not found" + }; + return await ServiceResult>.Failure(errors); } var likes = await _unitOfWork.CommentLikes.FindAllAsync( @@ -57,6 +61,7 @@ public async Task>> GetLikesForComment public async Task> AddLikeAsync(int commentId, int userId) { + using var transaction = await _unitOfWork.BeginTransactionAsync(); try { _logger.LogInformation("Adding like for comment id: {CommentId} by user id: {UserId}", @@ -70,6 +75,7 @@ public async Task> AddLikeAsync(int commentId, int { $"User with id {userId} not found" }; + await transaction.RollbackAsync(); return await ServiceResult.Failure(errors); } @@ -80,6 +86,7 @@ public async Task> AddLikeAsync(int commentId, int { $"Comment with id {commentId} not found" }; + await transaction.RollbackAsync(); return await ServiceResult.Failure(errors); } @@ -97,14 +104,17 @@ public async Task> AddLikeAsync(int commentId, int { "User has already liked this comment" }; + await transaction.RollbackAsync(); return await ServiceResult.Failure(errors); } - // otherwise , the user liked the commend , then unliked it , and then wants to like it again + // otherwise , the user liked the comment , then unliked it , and then wants to like it again existingLike.IsDeleted = false; comment.LikeCount++; _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); + await transaction.CommitAsync(); + _logger.LogInformation("Like added successfully for comment id: {CommentId}", commentId); @@ -131,6 +141,7 @@ public async Task> AddLikeAsync(int commentId, int _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); + await transaction.CommitAsync(); _logger.LogInformation("Like added successfully for comment id: {CommentId}", commentId); return await ServiceResult.Success(new CommentLikeDto @@ -143,6 +154,7 @@ public async Task> AddLikeAsync(int commentId, int } catch (Exception ex) { + await transaction.RollbackAsync(); _logger.LogError(ex, "Error adding like for comment id: {CommentId} by user id: {UserId}", commentId, userId); return await ServiceResult.Failure(new List(){ex.Message}); @@ -153,6 +165,7 @@ public async Task> AddLikeAsync(int commentId, int public async Task> DeleteLikeAsync(int commentId, int userId) { + var transaction = await _unitOfWork.BeginTransactionAsync(); try { _logger.LogInformation("Deleting the comment like from user {userid} on comment id {commentId}", userId, commentId); @@ -165,6 +178,7 @@ public async Task> DeleteLikeAsync(int commentId, { "$User didn't like this comment" }; + await transaction.RollbackAsync(); return await ServiceResult.Failure(errors); } @@ -181,6 +195,7 @@ public async Task> DeleteLikeAsync(int commentId, "User didn't like this comment" }; + await transaction.RollbackAsync(); return await ServiceResult.Failure(errors); } @@ -196,11 +211,12 @@ public async Task> DeleteLikeAsync(int commentId, _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); - + await transaction.CommitAsync(); return await ServiceResult.Success(); } catch (Exception e) { + await transaction.RollbackAsync(); _logger.LogError(e, "Failed to delete like by user {UserId} on comment {CommentId}", userId, commentId); return await ServiceResult.Failure(new List(){e.Message}); } diff --git a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs index c4fd6ec..042eedf 100644 --- a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs +++ b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs @@ -1,4 +1,5 @@ using AskFm.DAL.Models; +using Microsoft.EntityFrameworkCore.Storage; using Thread = AskFm.DAL.Models.Thread; namespace AskFm.DAL.Interfaces; @@ -16,5 +17,6 @@ public interface IUnitOfWork : IDisposable int Save(); Task SaveAsync(); - + + Task BeginTransactionAsync(); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/UnitOfWork.cs b/AskFm/AskFm.DAL/UnitOfWork.cs index a31c2f2..befcd52 100644 --- a/AskFm/AskFm.DAL/UnitOfWork.cs +++ b/AskFm/AskFm.DAL/UnitOfWork.cs @@ -1,6 +1,7 @@ using AskFm.DAL.Interfaces; using AskFm.DAL.Models; using AskFm.DAL.Repositories; +using Microsoft.EntityFrameworkCore.Storage; using Thread = AskFm.DAL.Models.Thread; namespace AskFm.DAL; @@ -134,4 +135,10 @@ public Task SaveAsync() { return _context.SaveChangesAsync(); } + + public async Task BeginTransactionAsync() + { + return await _context.Database.BeginTransactionAsync(); + } + } \ No newline at end of file From bca862571337e97d2147f233ac417d8ea34d3cc0 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sat, 30 Aug 2025 16:24:05 +0300 Subject: [PATCH 63/92] test: Fixed tests fails because of transactions --- AskFm/Tests/CommentLikeServiceTest.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AskFm/Tests/CommentLikeServiceTest.cs b/AskFm/Tests/CommentLikeServiceTest.cs index 266de4c..c022ccf 100644 --- a/AskFm/Tests/CommentLikeServiceTest.cs +++ b/AskFm/Tests/CommentLikeServiceTest.cs @@ -6,6 +6,8 @@ using Moq; using System.Linq.Expressions; using AskFm.BLL.DTO; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore.Storage; namespace Tests; @@ -18,6 +20,7 @@ public class CommentLikeServiceTest private readonly Mock> _mockUserRepository; private readonly Mock> _mockCommentLikeRepository; private readonly CommentLikeService _commentLikeService; + private readonly Mock _mockTransaction; public CommentLikeServiceTest() { @@ -26,9 +29,12 @@ public CommentLikeServiceTest() _mockCommentLikeRepository = new Mock>(); _mockUserRepository = new Mock>(); _loggerMock = new Mock>(); + _mockTransaction = new Mock(); _mockUnitOfWork.Setup(uow => uow.Comments).Returns(_mockCommentRepository.Object); _mockUnitOfWork.Setup(uow => uow.CommentLikes).Returns(_mockCommentLikeRepository.Object); _mockUnitOfWork.Setup(uow => uow.Users).Returns(_mockUserRepository.Object); + _mockUnitOfWork.Setup(uow => uow.BeginTransactionAsync()).ReturnsAsync(_mockTransaction.Object); + _commentLikeService = new CommentLikeService(_mockUnitOfWork.Object, _loggerMock.Object); @@ -40,6 +46,7 @@ public async Task AddLike_WhenCommentIsNotDeleted_AddingNewLikeOnTheComment() // Arrange var commentId = 1; var userId = 5; + var mockTransaction = new Mock(); var comment = new Comment { @@ -57,7 +64,8 @@ public async Task AddLike_WhenCommentIsNotDeleted_AddingNewLikeOnTheComment() _mockCommentRepository.Setup(repo => repo.GetByIdAsync(commentId)) .ReturnsAsync(comment); - + + _mockUserRepository.Setup(repo => repo.GetByIdAsync(userId)) .ReturnsAsync(user); From b6f706e362ed08057093c2b4872ff1a641a1b3bc Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Sat, 30 Aug 2025 19:36:05 +0300 Subject: [PATCH 64/92] Add unit tests: Complete Update & Delete user unit tests --- .../UserIdentityService/UserService.cs | 2 +- AskFm/Tests/UserTests.cs | 62 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 5119e8d..09b7130 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -44,7 +44,7 @@ public async Task> UpdateUserAsync(int userId, UpdateUserDTO public async Task> DeleteUserAsync(int userId) { - var appUser = _unitOfWork.Users.GetById(userId); + var appUser = await _unitOfWork.Users.GetByIdAsync(userId); var res = await CheckNullObjectAsync(appUser); if (!res.success) return res; diff --git a/AskFm/Tests/UserTests.cs b/AskFm/Tests/UserTests.cs index 849f524..79943d2 100644 --- a/AskFm/Tests/UserTests.cs +++ b/AskFm/Tests/UserTests.cs @@ -82,12 +82,39 @@ public async Task UpdatedUser_UpdatedWithCorrectUserAndCorrectData_SuccessWithUs [Fact] public async Task UpdatedUser_ThePassedUserIsNull_UpdateFaild() { + // Act + var result = await _userService.UpdateUserAsync(1, null); + // Assert + Assert.False(result.success); + _mockUnitOfWork.Verify((u=>u.Users.GetByIdAsync(It.IsAny())),Times.Never); + _mockUnitOfWork.Verify(u => u.Users.UpdateAsync(It.IsAny()), Times.Never); + _mockUnitOfWork.Verify(u => u.SaveAsync(), Times.Never); + } [Fact] public async Task UpdatedUser_ThePassedUserIsNotFound_UpdateFaild() { + // Arrange + int userId = 1; + var updatedUser = new UpdateUserDTO + { + Name = "John", + Bio = "this is John, software engineer.", + AvatarPath = "/image.jpg" + }; + + _mockUnitOfWork.Setup(u => u.Users.GetByIdAsync(userId)).ReturnsAsync((ApplicationUser?)null); + + // act + var result = await _userService.UpdateUserAsync(userId, updatedUser); + + // Assert + Assert.False(result.success); + _mockUnitOfWork.Verify((u=>u.Users.GetByIdAsync(It.IsAny())),Times.Once); + _mockUnitOfWork.Verify(u => u.Users.UpdateAsync(It.IsAny()), Times.Never); + _mockUnitOfWork.Verify(u => u.SaveAsync(), Times.Never); } @@ -97,18 +124,51 @@ public async Task UpdatedUser_ThePassedUserIsNotFound_UpdateFaild() [Fact] public async Task DeleteUserAsync_ThePassedUserNotFound_DeleteFaild() { + // Arrange + int userId = 1; + _mockUnitOfWork.Setup(u=>u.Users.GetByIdAsync(userId)).ReturnsAsync((ApplicationUser?)null); + // Act + var result = await _userService.DeleteUserAsync(userId); + // Assert + Assert.False(result.success); + _mockUnitOfWork.Verify((u=>u.Users.GetByIdAsync(userId)), Times.Once); + _mockUnitOfWork.Verify(u => u.Users.RemoveAsync(It.IsAny()), Times.Never); + _mockUnitOfWork.Verify(u => u.SaveAsync(), Times.Never); } [Fact] public async Task DeleteUserAsync_FoundedUser_DeleteSuccess() { + // Arrange + int userId = 1; + var appUser = new ApplicationUser + { + Id = userId, + Name = "Name", + Bio = "Bio", + AvatarPath = "/image.jpg" + }; + _mockUnitOfWork.Setup(u => u.Users.GetByIdAsync(userId)).ReturnsAsync(appUser); + _mockUnitOfWork.Setup(u=>u.Users.RemoveAsync(appUser)).Returns(Task.CompletedTask); + _mockUnitOfWork.Setup(u => u.SaveAsync()).ReturnsAsync(1); + + + // Act + var result = await _userService.DeleteUserAsync(userId); + + // + Assert.True(result.success); + _mockUnitOfWork.Verify(u => u.Users.GetByIdAsync(userId), Times.Once); + _mockUnitOfWork.Verify(u=>u.Users.RemoveAsync(appUser),Times.Once); + _mockUnitOfWork.Verify((u=>u.SaveAsync()), Times.Once); + + } - // helper functions private Mock> GetMockUserManager() From e5f86508a40cadffc88721c92d41293727bd6b04 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 4 Sep 2025 19:09:16 +0300 Subject: [PATCH 65/92] Implement Delete User unit tests --- AskFm/Tests/UserTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AskFm/Tests/UserTests.cs b/AskFm/Tests/UserTests.cs index 79943d2..85c32c0 100644 --- a/AskFm/Tests/UserTests.cs +++ b/AskFm/Tests/UserTests.cs @@ -167,8 +167,11 @@ public async Task DeleteUserAsync_FoundedUser_DeleteSuccess() } - - + /// + /// Follow 6, search how to test transactions + /// + /// + /// // helper functions private Mock> GetMockUserManager() From 40b73b0f8f388f76cea58b7b91932f8ed697aa8c Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 4 Sep 2025 19:16:20 +0300 Subject: [PATCH 66/92] Fix _checkCurrentUser Issue --- AskFm/AskFm.API/Controllers/UserController.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 85d32ef..830ba38 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -22,19 +22,6 @@ public UserController(IUnitOfWork unitOfWork, IAuthService authService, IUserSer _authService = authService; _userService = userService; } - [HttpGet] - [Route("GetAllUsers")] - public async Task GetUsers() - { - var result = _unitOfWork.Users.GetAll().Select(u => new - { - name = u.Name, - email = u.Email, - username = u.UserName, - bio = u.Bio, - }).ToList(); - return Ok(result); - } [HttpGet] [Route("profile")] @@ -151,7 +138,7 @@ public async Task UpdatePassword(int userId, string currentPasswo { if (await _checkCurrentUser(userId)) { - return Forbid("Cannot unFollow the current user"); + return Forbid("Cannot update password for another user"); } var result = await _userService.UpdatePassword(userId, currentPassword, updatedPassword); if (!result.success) @@ -168,11 +155,11 @@ public async Task UpdatePassword(int userId, string currentPasswo private async Task _checkCurrentUser(int userId) { var current_user = _userService.GetCurrentUserAsync(); - return current_user.Id != userId; + return current_user.Id == userId; } - /* + /* TODO update email confirm email check user not deleted in login From 419fa12df0cba7b24e8cb2a0fc33ce16894063cc Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 4 Sep 2025 19:38:24 +0300 Subject: [PATCH 67/92] Replace Redirect by return that data --- AskFm/AskFm.API/Controllers/UserController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 830ba38..423adc5 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -70,7 +70,9 @@ public async Task UpdateUserAsync(int userId, UpdateUserDTO updat { return BadRequest(result.Errors); } - return RedirectToAction("GetUserAsync", new { userId = userId }); + + var userRead = await _userService.GetUserByIdAsync(userId); + return Ok(userRead); } [HttpDelete] From cb3f3b365163c8c76b5deb5b2748349c2c548a2b Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 4 Sep 2025 19:52:03 +0300 Subject: [PATCH 68/92] refactor: created updatePassword DTO and use it instead of passing passwords in paramaters --- AskFm/AskFm.API/Controllers/UserController.cs | 5 +++-- AskFm/AskFm.BLL/DTO/UserDTOs/UpdatePasswordDTO.cs | 7 +++++++ .../Services/UserIdentityService/IUserService.cs | 2 +- .../Services/UserIdentityService/UserService.cs | 10 ++++++---- 4 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/UserDTOs/UpdatePasswordDTO.cs diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 423adc5..8c1df4a 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -136,13 +136,14 @@ public async Task UnFollowUserAsync(int followerId, int targetUse [HttpPost] [Route("profile/update/pass/{userId}")] - public async Task UpdatePassword(int userId, string currentPassword, string updatedPassword) + public async Task UpdatePassword(int userId, UpdatePasswordDTO udpatePasswordDto) { if (await _checkCurrentUser(userId)) { return Forbid("Cannot update password for another user"); } - var result = await _userService.UpdatePassword(userId, currentPassword, updatedPassword); + + var result = await _userService.UpdatePassword(userId, udpatePasswordDto); if (!result.success) { return BadRequest(result.Errors); diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/UpdatePasswordDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/UpdatePasswordDTO.cs new file mode 100644 index 0000000..1cba7a8 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/UpdatePasswordDTO.cs @@ -0,0 +1,7 @@ +namespace AskFm.BLL.DTO.UserDTOs; + +public class UpdatePasswordDTO +{ + public string CurrentPassword { get; set; } + public string UpdatedPassword { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs index f2aad1a..8a00ba2 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -12,7 +12,7 @@ public interface IUserService Task> UpdateLastSeenAsync(int userId); Task> GetUserByIdAsync(int userId); Task> GetCurrentUserAsync(); - Task> UpdatePassword(int userId, string currentPassword, string updatedPassword); + Task> UpdatePassword(int userId, UpdatePasswordDTO updatePasswordDto); Task> ResetEmail(int userId, string updatedEmail); Task> ConfirmEmail(); diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 09b7130..559102b 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -210,25 +210,27 @@ public async Task> GetCurrentUserAsync() return await ServiceResult.Success(currentAppUser); } - public async Task> UpdatePassword(int userId, string currentPassword, string updatedPassword) + public async Task> UpdatePassword(int userId, UpdatePasswordDTO updatePasswordDto) { + var nullPassRes = await CheckNullObjectAsync(updatePasswordDto); + if(!nullPassRes.success) return nullPassRes; var appUser = await _unitOfWork.Users.GetByIdAsync(userId); var res = await CheckNullObjectAsync(appUser); if (!res.success) return res; - var passwordValid = await _userManager.CheckPasswordAsync(appUser, updatedPassword); + var passwordValid = await _userManager.CheckPasswordAsync(appUser, updatePasswordDto.CurrentPassword); if (!passwordValid) { var errors = new List { "Invalid Password." }; return await ServiceResult.Failure(errors); } - if (currentPassword==updatedPassword) + if (updatePasswordDto.CurrentPassword==updatePasswordDto.UpdatedPassword) { var errors = new List { "It is the same old password." }; return await ServiceResult.Failure(errors); } - var result = await _userManager.ChangePasswordAsync(appUser, currentPassword, updatedPassword); + var result = await _userManager.ChangePasswordAsync(appUser, updatePasswordDto.CurrentPassword, updatePasswordDto.UpdatedPassword); if (!result.Succeeded) { var errors = new List { "Cannot Update Current Password." }; From 38a7ea69b1491db03f15082a7f74bb04afa4747c Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 4 Sep 2025 19:54:05 +0300 Subject: [PATCH 69/92] Fix: solve the issue in _checkCurrentUser --- AskFm/AskFm.API/Controllers/UserController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 8c1df4a..a88de46 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -157,8 +157,8 @@ public async Task UpdatePassword(int userId, UpdatePasswordDTO ud // Helper functions private async Task _checkCurrentUser(int userId) { - var current_user = _userService.GetCurrentUserAsync(); - return current_user.Id == userId; + var current_user = await _userService.GetCurrentUserAsync(); + return current_user.Data.Id == userId; } From 7a78b43d59b926a56b3864f3da25dac50f8d7a13 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 4 Sep 2025 19:59:09 +0300 Subject: [PATCH 70/92] Fix: Solve the wrong return in checking object null in follow and unfollow function userService --- .../Services/UserIdentityService/UserService.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 559102b..e21609a 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -65,12 +65,12 @@ public async Task> FollowUserAsync(int followerId, int targe try { var userFollower = await _unitOfWork.Users.GetByIdAsync(followerId); - var res = await CheckNullObjectAsync(userFollower); - if (!res.success) return res; + var userFollowerNullRes = await CheckNullObjectAsync(userFollower); + if (!userFollowerNullRes.success) return userFollowerNullRes; var targetUser = await _unitOfWork.Users.GetByIdAsync(targetUserId); - var res2 = await CheckNullObjectAsync(targetUser); - if (!res2.success) return res; + var targetUserNullRes = await CheckNullObjectAsync(targetUser); + if (!targetUserNullRes.success) return targetUserNullRes; var followExist = await _unitOfWork.Follows.GetAll() .FirstOrDefaultAsync(f => f.FollowedId == targetUserId @@ -126,12 +126,12 @@ public async Task> UnfollowUserAsync(int followerId, int tar try { var userFollower = await _unitOfWork.Users.GetByIdAsync(followerId); - var res = await CheckNullObjectAsync(userFollower); - if (!res.success) return res; + var userFollowerNullRes = await CheckNullObjectAsync(userFollower); + if (!userFollowerNullRes.success) return userFollowerNullRes; var targetUser = await _unitOfWork.Users.GetByIdAsync(targetUserId); - var res2 = await CheckNullObjectAsync(targetUser); - if (!res2.success) return res; + var targetUserNullRes = await CheckNullObjectAsync(targetUser); + if (!targetUserNullRes.success) return targetUserNullRes; var followExist = await _unitOfWork.Follows.GetAll() From 909ca8b4890e9af5ad222fe214679df941016077 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 4 Sep 2025 22:08:45 +0300 Subject: [PATCH 71/92] fix: fix unfollow --- .../AskFm.BLL/Services/UserIdentityService/UserService.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index e21609a..35ca297 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -141,8 +141,14 @@ public async Task> UnfollowUserAsync(int followerId, int tar if (followExist != null) { followExist.IsDeleted = true; + followExist.IsDeleted = true; + followExist.IsActive = false; + if (userFollower.FollowingCount > 0) userFollower.FollowingCount--; + if (targetUser.FollowersCount > 0) targetUser.FollowersCount--; + await _unitOfWork.Follows.UpdateAsync(followExist); + await _unitOfWork.Users.UpdateAsync(userFollower); + await _unitOfWork.Users.UpdateAsync(targetUser); await _unitOfWork.SaveAsync(); - } transaction.Commit(); From afc5f7065e46e13deed5c5013c023d2642387eb9 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 4 Sep 2025 22:13:57 +0300 Subject: [PATCH 72/92] fix: calling save changes in update last seen --- AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 35ca297..5e755d1 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -172,6 +172,7 @@ public async Task> UpdateLastSeenAsync(int userId) { appUser.LastSeen = DateTime.Now; await _unitOfWork.Users.UpdateAsync(appUser); + await _unitOfWork.SaveAsync(); await transaction.CommitAsync(); return await ServiceResult.Success(true); } From 29e1eeb584b52261105b5110ba72220e801c2b57 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 4 Sep 2025 23:18:33 +0300 Subject: [PATCH 73/92] Refactor: adding query filter to filter the deleted raws in users, comments, threads and notifications --- AskFm/AskFm.API/Controllers/UserController.cs | 17 +++++++++-------- .../Services/UserIdentityService/AuthService.cs | 2 +- AskFm/AskFm.DAL/AppDbContext.cs | 4 ++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index a88de46..3f60899 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -63,7 +63,7 @@ public async Task UpdateUserAsync(int userId, UpdateUserDTO updat { if (!await _checkCurrentUser(userId)) { - return Forbid("Cannot Update this user"); + return StatusCode(StatusCodes.Status403Forbidden, "Cannot Update this user"); } var result = await _userService.UpdateUserAsync(userId, updatedUser); if (!result.success) @@ -81,7 +81,7 @@ public async Task DeleteUserAsync(int userId) { if (!await _checkCurrentUser(userId)) { - return Forbid("Cannot Remove this user"); + return StatusCode(StatusCodes.Status403Forbidden, "Cannot Remove this user"); } var result = await _userService.DeleteUserAsync(userId); @@ -98,11 +98,11 @@ public async Task FollowUserAsync(int followerId, int targetUserI { if (await _checkCurrentUser(targetUserId)) { - return Forbid("Cannot Follow the current user"); + return StatusCode(StatusCodes.Status403Forbidden, "Cannot Follow this user"); } if (!await _checkCurrentUser(followerId)) { - return Forbid("User can't perform this follow"); + return StatusCode(StatusCodes.Status403Forbidden, "User can't perform this follow"); } var result = await _userService.FollowUserAsync(followerId, targetUserId); @@ -119,11 +119,11 @@ public async Task UnFollowUserAsync(int followerId, int targetUse { if (await _checkCurrentUser(targetUserId)) { - return Forbid("Cannot unFollow the current user"); + return StatusCode(StatusCodes.Status403Forbidden, "Cannot Unfollow the current user"); } if (!await _checkCurrentUser(followerId)) { - return Forbid("User can't perform this unfollow"); + return StatusCode(StatusCodes.Status403Forbidden, "User can't perform this unfollow"); } var result = await _userService.UnfollowUserAsync(followerId, targetUserId); @@ -140,10 +140,11 @@ public async Task UpdatePassword(int userId, UpdatePasswordDTO ud { if (await _checkCurrentUser(userId)) { - return Forbid("Cannot update password for another user"); + + return StatusCode(StatusCodes.Status403Forbidden, "Invalid Operation"); } - var result = await _userService.UpdatePassword(userId, udpatePasswordDto); + if (!result.success) { return BadRequest(result.Errors); diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs index 3a0b8c7..a6ca0f1 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs @@ -80,7 +80,7 @@ public async Task> RegisterAsync(RegisterUserDTO return await ServiceResult.Failure(errors); } var oldUser = _userManager.FindByEmailAsync(request.Email).Result; - if (oldUser != null) + if (oldUser != null && oldUser.IsDeleted ) { var errors = new List{ "Email already exist" }; return await ServiceResult.Failure(errors); diff --git a/AskFm/AskFm.DAL/AppDbContext.cs b/AskFm/AskFm.DAL/AppDbContext.cs index 7c9196c..158cc1e 100644 --- a/AskFm/AskFm.DAL/AppDbContext.cs +++ b/AskFm/AskFm.DAL/AppDbContext.cs @@ -47,6 +47,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnType("BIT") .HasDefaultValue(false); } + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); } From c04f17d61b68dc01adcb9ad3d66339d4994b388a Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 4 Sep 2025 23:34:09 +0300 Subject: [PATCH 74/92] Calling GetByIdAsync instead of sync --- AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 5e755d1..7dcc1a9 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -186,7 +186,7 @@ public async Task> UpdateLastSeenAsync(int userId) public async Task> GetUserByIdAsync(int userId) { - var user = _unitOfWork.Users.GetById(userId); + var user = await _unitOfWork.Users.GetByIdAsync(userId); var res = await CheckNullObjectAsync(user); if (!res.success) return res; From e96d3e24c26d1e6857d64ab7df882ed9b68e06c8 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Sun, 7 Sep 2025 02:17:17 +0300 Subject: [PATCH 75/92] Add: Adding Docs for APIs --- APIs/Auth.md | 149 ++++++++++++++++++ APIs/User.md | 105 ++++++++++++ AskFm/AskFm.API/Controllers/AuthController.cs | 2 +- AskFm/AskFm.API/Controllers/UserController.cs | 2 +- 4 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 APIs/Auth.md create mode 100644 APIs/User.md diff --git a/APIs/Auth.md b/APIs/Auth.md new file mode 100644 index 0000000..1918d5a --- /dev/null +++ b/APIs/Auth.md @@ -0,0 +1,149 @@ +# Endpoints for Authentication + +### Register +POST: `/api/Auth/register` + +**Request** +```json +{ + "name": "string", + "username": "string", + "email": "string", + "bio": "string", + "avatarPath": "string", + "passwrod": "string", + "lastSeen": "2025-09-06T22:27:00.026Z" +} +``` +**Response** + +Status Code: 200 + +```json +{ + "success": true, + "errors": [ + "string" + ], + "data": { + "isAuthenticated": true, + "token": "string", + "refreshToken": { + "token": "string", + "expireOn": "2025-09-06T22:57:05.109Z", + "isExpired": true, + "revokedOn": "2025-09-06T22:57:05.109Z", + "isActive": true, + "createdOn": "2025-09-06T22:57:05.109Z" + }, + "user": { + "name": "string", + "email": "string", + "lastSeen": "2025-09-06T22:57:05.109Z", + "bio": "string", + "avatarPath": "string", + "followerCount": 0 + } + } +} +``` + +----- +### Login +POST: `/api/Auth/login` + +**Request** +```json +{ + "email": "string", + "password": "string" + +} +``` +**Response** + +Status Code: 200 + +```json +{ + "success": true, + "errors": [ + "string" + ], + "data": { + "isAuthenticated": true, + "token": "string", + "refreshToken": { + "token": "string", + "expireOn": "2025-09-06T22:57:05.109Z", + "isExpired": true, + "revokedOn": "2025-09-06T22:57:05.109Z", + "isActive": true, + "createdOn": "2025-09-06T22:57:05.109Z" + }, + "user": { + "name": "string", + "email": "string", + "lastSeen": "2025-09-06T22:57:05.109Z", + "bio": "string", + "avatarPath": "string", + "followerCount": 0 + } + } +} +``` + +------ +### Refresh Token +POST: `/api/Auth/refresh-token/{id}` +Description: refersh token when jwt token is expired + +**Response** + +Status Code: 200 +```json +{ + "success": true, + "errors": [ + "string" + ], + "data": { + "isAuthenticated": true, + "token": "string", + "refreshToken": { + "token": "string", + "expireOn": "2025-09-06T22:57:05.109Z", + "isExpired": true, + "revokedOn": "2025-09-06T22:57:05.109Z", + "isActive": true, + "createdOn": "2025-09-06T22:57:05.109Z" + }, + "user": { + "name": "string", + "email": "string", + "lastSeen": "2025-09-06T22:57:05.109Z", + "bio": "string", + "avatarPath": "string", + "followerCount": 0 + } + } +} +``` + +--------- +### Logout +POST: `/api/Auth/logout/{id}` +Description: refersh token when jwt token is expired + +**Response** + +Status Code: 200 +```json +{ + "success": true, + "errors": [ + "string" + ], + "data": true +} +``` diff --git a/APIs/User.md b/APIs/User.md new file mode 100644 index 0000000..8fb39fa --- /dev/null +++ b/APIs/User.md @@ -0,0 +1,105 @@ +# Endpoints for User +### Get current user Profile +GET: `/api/User/profile` + +**Response** + +Status Code: 200 + +```json +{ + "name": "string", + "email": "string", + "lastSeen": "2025-09-06T23:04:42.634Z", + "bio": "string", + "avatarPath": "string", + "followerCount": 0 +} +``` + +--------- +### Get User By +GET: `/api/User/profile/{userId}` +Description: Get user by id + +**Response** + +Status Code: 200 + +```json +{ + "name": "string", + "email": "string", + "lastSeen": "2025-09-06T23:04:42.634Z", + "bio": "string", + "avatarPath": "string", + "followerCount": 0 +} +``` + +--------- +### Delete current user +Delete: `/api/User/profile/{userId}` + +**Response** +Status Code: 200 + +--------- +### Update User +POST: `/api/User/profile/update/{userId}` + +**Request** +```json +{ + "name": "string", + "bio": "string", + "avatarPath": "string" +} +``` + +**Response** + +Status Code: 200 + +```json +{ + "name": "string", + "email": "string", + "lastSeen": "2025-09-06T23:10:21.411Z", + "bio": "string", + "avatarPath": "string", + "followerCount": 0 +} +``` + +--------- +### Follow +POST: `/api/User/profile/{followerId}/follow/{targetUserId}` + +**Response** + +Status Code: 200 + +--------- +### Unfollow +POST: `/api/User/profile/{followerId}/unfollow/{targetUserId}` + +**Response** + +Status Code: 200 + +--------- +### Update Password +GET: `/api/User/profile/update/pass/{userId}` + +**Request** +```json +{ + "currentPassword": "string", + "updatedPassword": "string" +} +``` + +**Response** + +Status Code: 200 diff --git a/AskFm/AskFm.API/Controllers/AuthController.cs b/AskFm/AskFm.API/Controllers/AuthController.cs index 355b213..9760609 100644 --- a/AskFm/AskFm.API/Controllers/AuthController.cs +++ b/AskFm/AskFm.API/Controllers/AuthController.cs @@ -56,7 +56,7 @@ public async Task Login(LoginDTO login) return Ok(result); } - [HttpGet] + [HttpPost] [Route("refresh-token/{id}")] [Authorize(AuthenticationSchemes = "Bearer")] public async Task RefreshToken(int id) diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 3f60899..894daed 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -72,7 +72,7 @@ public async Task UpdateUserAsync(int userId, UpdateUserDTO updat } var userRead = await _userService.GetUserByIdAsync(userId); - return Ok(userRead); + return Ok(userRead.Data); } [HttpDelete] From 7a0c157515c1935ca697dc4c0593e36325409122 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Tue, 9 Sep 2025 04:56:33 +0300 Subject: [PATCH 76/92] Redis step (locally) --- AskFm/AskFm.API/AskFm.API.csproj | 2 + AskFm/AskFm.API/Program.cs | 10 +++++ AskFm/AskFm.API/appsettings.json | 3 +- AskFm/AskFm.BLL/Services/RedisCacheService.cs | 37 +++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 AskFm/AskFm.BLL/Services/RedisCacheService.cs diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index 77d8e4c..59c3c20 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -18,7 +18,9 @@ + + diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index c06620b..efe32e8 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -150,6 +150,16 @@ public static void Main(string[] args) }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); + + + builder.Services.AddStackExchangeRedisCache(options => + { + options.Configuration = builder.Configuration.GetConnectionString("Redis"); + options.InstanceName = "AskFmCache"; + }); + + builder.Services.AddSingleton(); + var app = builder.Build(); diff --git a/AskFm/AskFm.API/appsettings.json b/AskFm/AskFm.API/appsettings.json index 779f8b0..0ea25cc 100644 --- a/AskFm/AskFm.API/appsettings.json +++ b/AskFm/AskFm.API/appsettings.json @@ -1,6 +1,7 @@ { "ConnectionStrings": { - "DefaultConnection": "CONNECTIONSTRING" + "DefaultConnection": "CONNECTIONSTRING", + "Redis": "localhost:6379" }, "Logging": { "LogLevel": { diff --git a/AskFm/AskFm.BLL/Services/RedisCacheService.cs b/AskFm/AskFm.BLL/Services/RedisCacheService.cs new file mode 100644 index 0000000..03ae1be --- /dev/null +++ b/AskFm/AskFm.BLL/Services/RedisCacheService.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace AskFm.BLL.Services; + +public class RedisCacheService +{ + private readonly IDistributedCache _cache; + + public RedisCacheService(IDistributedCache cache) + { + _cache = cache; + } + + public async Task SetCacheAsync(string key, T value, TimeSpan expirationTime) + { + var options = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = expirationTime + }; + + var jsonData = JsonSerializer.Serialize(value); + await _cache.SetStringAsync(key, jsonData, options); + } + + public async Task GetCacheAsync(string key) + { + var jsonData = await _cache.GetStringAsync(key); + return jsonData is null ? default : JsonSerializer.Deserialize(jsonData); + } + + public async Task RemoveCacheAsync(string key) + { + await _cache.RemoveAsync(key); + } +} + From 4008027aa935c2ec2d3a0af974c6ae22110e1a3b Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Tue, 9 Sep 2025 06:22:06 +0300 Subject: [PATCH 77/92] Revert "Add: Adding Docs for APIs" This reverts commit e96d3e24c26d1e6857d64ab7df882ed9b68e06c8. --- APIs/Auth.md | 149 ------------------ APIs/User.md | 105 ------------ AskFm/AskFm.API/Controllers/AuthController.cs | 2 +- AskFm/AskFm.API/Controllers/UserController.cs | 2 +- 4 files changed, 2 insertions(+), 256 deletions(-) delete mode 100644 APIs/Auth.md delete mode 100644 APIs/User.md diff --git a/APIs/Auth.md b/APIs/Auth.md deleted file mode 100644 index 1918d5a..0000000 --- a/APIs/Auth.md +++ /dev/null @@ -1,149 +0,0 @@ -# Endpoints for Authentication - -### Register -POST: `/api/Auth/register` - -**Request** -```json -{ - "name": "string", - "username": "string", - "email": "string", - "bio": "string", - "avatarPath": "string", - "passwrod": "string", - "lastSeen": "2025-09-06T22:27:00.026Z" -} -``` -**Response** - -Status Code: 200 - -```json -{ - "success": true, - "errors": [ - "string" - ], - "data": { - "isAuthenticated": true, - "token": "string", - "refreshToken": { - "token": "string", - "expireOn": "2025-09-06T22:57:05.109Z", - "isExpired": true, - "revokedOn": "2025-09-06T22:57:05.109Z", - "isActive": true, - "createdOn": "2025-09-06T22:57:05.109Z" - }, - "user": { - "name": "string", - "email": "string", - "lastSeen": "2025-09-06T22:57:05.109Z", - "bio": "string", - "avatarPath": "string", - "followerCount": 0 - } - } -} -``` - ------ -### Login -POST: `/api/Auth/login` - -**Request** -```json -{ - "email": "string", - "password": "string" - -} -``` -**Response** - -Status Code: 200 - -```json -{ - "success": true, - "errors": [ - "string" - ], - "data": { - "isAuthenticated": true, - "token": "string", - "refreshToken": { - "token": "string", - "expireOn": "2025-09-06T22:57:05.109Z", - "isExpired": true, - "revokedOn": "2025-09-06T22:57:05.109Z", - "isActive": true, - "createdOn": "2025-09-06T22:57:05.109Z" - }, - "user": { - "name": "string", - "email": "string", - "lastSeen": "2025-09-06T22:57:05.109Z", - "bio": "string", - "avatarPath": "string", - "followerCount": 0 - } - } -} -``` - ------- -### Refresh Token -POST: `/api/Auth/refresh-token/{id}` -Description: refersh token when jwt token is expired - -**Response** - -Status Code: 200 -```json -{ - "success": true, - "errors": [ - "string" - ], - "data": { - "isAuthenticated": true, - "token": "string", - "refreshToken": { - "token": "string", - "expireOn": "2025-09-06T22:57:05.109Z", - "isExpired": true, - "revokedOn": "2025-09-06T22:57:05.109Z", - "isActive": true, - "createdOn": "2025-09-06T22:57:05.109Z" - }, - "user": { - "name": "string", - "email": "string", - "lastSeen": "2025-09-06T22:57:05.109Z", - "bio": "string", - "avatarPath": "string", - "followerCount": 0 - } - } -} -``` - ---------- -### Logout -POST: `/api/Auth/logout/{id}` -Description: refersh token when jwt token is expired - -**Response** - -Status Code: 200 -```json -{ - "success": true, - "errors": [ - "string" - ], - "data": true -} -``` diff --git a/APIs/User.md b/APIs/User.md deleted file mode 100644 index 8fb39fa..0000000 --- a/APIs/User.md +++ /dev/null @@ -1,105 +0,0 @@ -# Endpoints for User -### Get current user Profile -GET: `/api/User/profile` - -**Response** - -Status Code: 200 - -```json -{ - "name": "string", - "email": "string", - "lastSeen": "2025-09-06T23:04:42.634Z", - "bio": "string", - "avatarPath": "string", - "followerCount": 0 -} -``` - ---------- -### Get User By -GET: `/api/User/profile/{userId}` -Description: Get user by id - -**Response** - -Status Code: 200 - -```json -{ - "name": "string", - "email": "string", - "lastSeen": "2025-09-06T23:04:42.634Z", - "bio": "string", - "avatarPath": "string", - "followerCount": 0 -} -``` - ---------- -### Delete current user -Delete: `/api/User/profile/{userId}` - -**Response** -Status Code: 200 - ---------- -### Update User -POST: `/api/User/profile/update/{userId}` - -**Request** -```json -{ - "name": "string", - "bio": "string", - "avatarPath": "string" -} -``` - -**Response** - -Status Code: 200 - -```json -{ - "name": "string", - "email": "string", - "lastSeen": "2025-09-06T23:10:21.411Z", - "bio": "string", - "avatarPath": "string", - "followerCount": 0 -} -``` - ---------- -### Follow -POST: `/api/User/profile/{followerId}/follow/{targetUserId}` - -**Response** - -Status Code: 200 - ---------- -### Unfollow -POST: `/api/User/profile/{followerId}/unfollow/{targetUserId}` - -**Response** - -Status Code: 200 - ---------- -### Update Password -GET: `/api/User/profile/update/pass/{userId}` - -**Request** -```json -{ - "currentPassword": "string", - "updatedPassword": "string" -} -``` - -**Response** - -Status Code: 200 diff --git a/AskFm/AskFm.API/Controllers/AuthController.cs b/AskFm/AskFm.API/Controllers/AuthController.cs index 9760609..355b213 100644 --- a/AskFm/AskFm.API/Controllers/AuthController.cs +++ b/AskFm/AskFm.API/Controllers/AuthController.cs @@ -56,7 +56,7 @@ public async Task Login(LoginDTO login) return Ok(result); } - [HttpPost] + [HttpGet] [Route("refresh-token/{id}")] [Authorize(AuthenticationSchemes = "Bearer")] public async Task RefreshToken(int id) diff --git a/AskFm/AskFm.API/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs index 894daed..3f60899 100644 --- a/AskFm/AskFm.API/Controllers/UserController.cs +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -72,7 +72,7 @@ public async Task UpdateUserAsync(int userId, UpdateUserDTO updat } var userRead = await _userService.GetUserByIdAsync(userId); - return Ok(userRead.Data); + return Ok(userRead); } [HttpDelete] From 45fbc7a77c5813a43de4354c4fa24fc7a51c9859 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Tue, 9 Sep 2025 07:48:23 +0300 Subject: [PATCH 78/92] reids service interface --- AskFm/AskFm.BLL/Services/IRedisService.cs | 14 ++++++++++++++ AskFm/AskFm.BLL/Services/RedisCacheService.cs | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 AskFm/AskFm.BLL/Services/IRedisService.cs diff --git a/AskFm/AskFm.BLL/Services/IRedisService.cs b/AskFm/AskFm.BLL/Services/IRedisService.cs new file mode 100644 index 0000000..ae55879 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IRedisService.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace AskFm.BLL.Services; + +public interface IRedisService +{ + public Task SetCacheAsync(string key, T value, TimeSpan expirationTime); + + public Task GetCacheAsync(string key); + + public Task RemoveCacheAsync(string key); +} + diff --git a/AskFm/AskFm.BLL/Services/RedisCacheService.cs b/AskFm/AskFm.BLL/Services/RedisCacheService.cs index 03ae1be..0c95a83 100644 --- a/AskFm/AskFm.BLL/Services/RedisCacheService.cs +++ b/AskFm/AskFm.BLL/Services/RedisCacheService.cs @@ -3,7 +3,7 @@ namespace AskFm.BLL.Services; -public class RedisCacheService +public class RedisCacheService : IRedisService { private readonly IDistributedCache _cache; From 8e022aaf82238845c97a7e93d6eab902b18e6425 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Tue, 9 Sep 2025 07:48:23 +0300 Subject: [PATCH 79/92] reids service interface --- AskFm/AskFm.BLL/Services/IRedisService.cs | 14 ++++++++++++++ AskFm/AskFm.BLL/Services/RedisCacheService.cs | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 AskFm/AskFm.BLL/Services/IRedisService.cs diff --git a/AskFm/AskFm.BLL/Services/IRedisService.cs b/AskFm/AskFm.BLL/Services/IRedisService.cs new file mode 100644 index 0000000..ae55879 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IRedisService.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace AskFm.BLL.Services; + +public interface IRedisService +{ + public Task SetCacheAsync(string key, T value, TimeSpan expirationTime); + + public Task GetCacheAsync(string key); + + public Task RemoveCacheAsync(string key); +} + diff --git a/AskFm/AskFm.BLL/Services/RedisCacheService.cs b/AskFm/AskFm.BLL/Services/RedisCacheService.cs index 03ae1be..0c95a83 100644 --- a/AskFm/AskFm.BLL/Services/RedisCacheService.cs +++ b/AskFm/AskFm.BLL/Services/RedisCacheService.cs @@ -3,7 +3,7 @@ namespace AskFm.BLL.Services; -public class RedisCacheService +public class RedisCacheService : IRedisService { private readonly IDistributedCache _cache; From 1c774c520ee78daafb1fdf247f887e1199f931fb Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Thu, 11 Sep 2025 05:27:09 +0300 Subject: [PATCH 80/92] Feat: - using redis to cache jwt token and refresh - immediate logout refactor: - change the http method of refressh token to be post instead of get - create AppConstants in shared layer for hardcoded variables or names --- AskFm/AskFm.API/Controllers/AuthController.cs | 16 ++- AskFm/AskFm.API/Program.cs | 27 ++++- AskFm/AskFm.API/appsettings.json | 4 + AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs | 11 ++ .../AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs | 2 +- .../UserIdentityService/AuthService.cs | 113 +++++++++++------- .../UserIdentityService/IAuthService.cs | 1 + .../UserIdentityService/JwtOptions.cs | 6 +- AskFm/AskFm.DAL/Models/ApplicationUser.cs | 2 +- AskFm/AskFm.DAL/Models/RefreshToken.cs | 28 ++--- AskFm/Shared/AppConstants.cs | 20 ++++ 11 files changed, 154 insertions(+), 76 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs create mode 100644 AskFm/Shared/AppConstants.cs diff --git a/AskFm/AskFm.API/Controllers/AuthController.cs b/AskFm/AskFm.API/Controllers/AuthController.cs index 355b213..1e06897 100644 --- a/AskFm/AskFm.API/Controllers/AuthController.cs +++ b/AskFm/AskFm.API/Controllers/AuthController.cs @@ -13,10 +13,12 @@ namespace AskFm.API.Controllers; public class AuthController : ControllerBase { private IAuthService _authService; + private IUserService _userService; - public AuthController(IAuthService authService) + public AuthController(IAuthService authService, IUserService userService) { _authService = authService; + _userService = userService; } [HttpPost] @@ -56,7 +58,7 @@ public async Task Login(LoginDTO login) return Ok(result); } - [HttpGet] + [HttpPost] [Route("refresh-token/{id}")] [Authorize(AuthenticationSchemes = "Bearer")] public async Task RefreshToken(int id) @@ -80,20 +82,24 @@ public async Task RefreshToken(int id) [Authorize(AuthenticationSchemes = "Bearer")] public async Task Logout(int id) { + var currentUser = await _userService.GetCurrentUserAsync(); + if (currentUser.Data == null || currentUser.Data.Id != id) + { + return BadRequest("Invalid data"); + } string refreshToken = Request.Cookies["refreshToken"]; if (string.IsNullOrEmpty(refreshToken)) { return BadRequest("token Is required"); } - ServiceResult result = await _authService.RevokeRefreshTokenAsync(id,refreshToken); - + var result = await _authService.Logout(currentUser.Data.Id,refreshToken); + if (!result.success) { return BadRequest(result.Errors); } - return Ok(result); } diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index efe32e8..b60f897 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -14,6 +14,7 @@ using Microsoft.OpenApi.Models; using AskFm.BLL.Services.UserIdentityService; using Castle.Components.DictionaryAdapter.Xml; +using Shared; namespace AskFm.API; @@ -51,7 +52,6 @@ public static void Main(string[] args) builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(setup => { - // Include 'SecurityScheme' to use JWT Authentication var jwtSecurityScheme = new OpenApiSecurityScheme { BearerFormat = "JWT", @@ -83,8 +83,8 @@ public static void Main(string[] args) Issuer = Environment.GetEnvironmentVariable("ISSUER"), Audience = Environment.GetEnvironmentVariable("AUDIENCE"), SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"), - AccessExpiration = int.Parse(Environment.GetEnvironmentVariable("TOKEN_EXP")), - AccessRefreshTokenExpiration = int.Parse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXP")), + AccessExpiration = builder.Configuration.GetValue("ExpireTimes:Jwt_Token_Exp"), + AccessRefreshTokenExpiration =builder.Configuration.GetValue("ExpireTimes:Refresh_Token_Exp") }; if (jwtOptions == null) { @@ -96,8 +96,8 @@ public static void Main(string[] args) Options.Issuer = Environment.GetEnvironmentVariable("ISSUER"); Options.Audience = Environment.GetEnvironmentVariable("AUDIENCE"); Options.SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"); - Options.AccessExpiration = int.Parse(Environment.GetEnvironmentVariable("TOKEN_EXP")); - Options.AccessRefreshTokenExpiration = int.Parse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXP")); + Options.AccessExpiration = builder.Configuration.GetValue("ExpireTimes:Jwt_Token_Exp"); + Options.AccessRefreshTokenExpiration = builder.Configuration.GetValue("ExpireTimes:Refresh_Token_Exp"); }); builder.Services.AddAuthentication(options => @@ -105,6 +105,7 @@ public static void Main(string[] args) options.DefaultAuthenticateScheme = "Bearer"; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) .AddJwtBearer( Options => { @@ -119,6 +120,21 @@ public static void Main(string[] args) IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)), ClockSkew = TimeSpan.FromMinutes(0) }; + Options.Events = new JwtBearerEvents + { + OnTokenValidated = async context => + { + var jti = context.Principal.Claims.FirstOrDefault(c => c.Type == "jti")?.Value; + var redis = context.HttpContext.RequestServices.GetRequiredService(); + + var cachedToken = await redis.GetCacheAsync(AppConstants.JwtCacheKey(jti)); + if (cachedToken <= 0) + { + context.Fail("Token revoked or expired"); + } + } + }; + }); builder.Services.AddAuthorization(); @@ -174,7 +190,6 @@ public static void Main(string[] args) app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); - app.MapControllers(); diff --git a/AskFm/AskFm.API/appsettings.json b/AskFm/AskFm.API/appsettings.json index 0ea25cc..177edc2 100644 --- a/AskFm/AskFm.API/appsettings.json +++ b/AskFm/AskFm.API/appsettings.json @@ -9,5 +9,9 @@ "Microsoft.AspNetCore": "Warning" } }, + "ExpireTimes": { + "Jwt_Token_Exp":10, + "Refresh_Token_Exp":30 + }, "AllowedHosts": "*" } diff --git a/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs new file mode 100644 index 0000000..cf524b2 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; + +namespace AskFm.BLL.DTO; +public class RefreshTokenDto +{ + public string Token { get; set; } + public DateTime ExpireOn { get; set; } + public bool IsExpired => DateTime.Now >= ExpireOn; + public int ExpireAfter { get; set; } + public DateTime CreatedOn { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs index 7948440..aa3ebba 100644 --- a/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs @@ -6,6 +6,6 @@ public class AuthResponseDTO { public bool IsAuthenticated { get; set; } public string Token { get; set; } - public RefreshToken RefreshToken { get; set; } + public RefreshTokenDto RefreshToken { get; set; } public ReadUserDTO User { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs index a6ca0f1..8662d25 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using System.Security.Cryptography; using System.Text; +using AskFm.BLL.DTO; using AskFm.BLL.DTO.UserDTOs; using AskFm.DAL; using AskFm.DAL.Interfaces; @@ -10,9 +11,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Server.HttpSys; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; - +using Shared; namespace AskFm.BLL.Services.UserIdentityService; public class AuthService : IAuthService @@ -20,11 +22,15 @@ public class AuthService : IAuthService private IUnitOfWork _unitOfWork; private readonly UserManager _userManager; private readonly JwtOptions _jwtOptions; + private readonly RedisCacheService _redisCacheService; + private readonly IConfiguration _configuration; - public AuthService(IUnitOfWork unitOfWork, UserManager userManager, IOptions jwtOptions) + public AuthService(IUnitOfWork unitOfWork, UserManager userManager, IOptions jwtOptions, RedisCacheService redisCacheService, IConfiguration configuration) { _unitOfWork = unitOfWork; _userManager = userManager; + _redisCacheService = redisCacheService; + _configuration = configuration; _jwtOptions = jwtOptions.Value; } @@ -80,7 +86,7 @@ public async Task> RegisterAsync(RegisterUserDTO return await ServiceResult.Failure(errors); } var oldUser = _userManager.FindByEmailAsync(request.Email).Result; - if (oldUser != null && oldUser.IsDeleted ) + if (oldUser != null && oldUser.IsDeleted) { var errors = new List{ "Email already exist" }; return await ServiceResult.Failure(errors); @@ -113,7 +119,7 @@ public async Task> RegisterAsync(RegisterUserDTO public async Task> RefreshTokenAsync(int id, string refreshToken) { - if (refreshToken == null) + if (string.IsNullOrEmpty(refreshToken)) { var errors = new List { "Invalid Token." }; return await ServiceResult.Failure(errors); @@ -125,23 +131,17 @@ public async Task> RefreshTokenAsync(int id, stri var errors = new List { "Invalid User." }; return await ServiceResult.Failure(errors); } - if (!user.RefreshTokens.Any(r => r.Token == refreshToken)) + + var oldRefreshToken = await _redisCacheService.GetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); + if (oldRefreshToken == null + || oldRefreshToken.IsExpired + || oldRefreshToken.Token != refreshToken) { var errors = new List { "Invalid Token." }; return await ServiceResult.Failure(errors); } - - var oldRefreshToken = user.RefreshTokens.Single(t => t.Token == refreshToken); - if (!oldRefreshToken.IsActive) - { - var errors = new List { "InActive Token." }; - return await ServiceResult.Failure(errors); - } - - oldRefreshToken.RevokedOn = DateTime.UtcNow; - + await _redisCacheService.RemoveCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); return await GetAuthToken(user); - } @@ -153,57 +153,67 @@ public async Task> RevokeRefreshTokenAsync(int id, string re return await ServiceResult.Failure(errors); } - var user = _unitOfWork.Users.GetById(id); + var user = await _unitOfWork.Users.GetByIdAsync(id); if (user == null) { var errors = new List { "Invalid User." }; return await ServiceResult.Failure(errors); } - if (!user.RefreshTokens.Any(r => r.Token == refreshToken)) + var userRefreshToken = await _redisCacheService.GetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); + if (userRefreshToken == null || userRefreshToken.IsExpired) { var errors = new List { "Invalid Token." }; return await ServiceResult.Failure(errors); } - - var oldRefreshToken = user.RefreshTokens.Single(t => t.Token == refreshToken); - - if (!oldRefreshToken.IsActive) - { - var errors = new List { "InActive Token." }; - return await ServiceResult.Failure(errors); - } - - oldRefreshToken.RevokedOn = DateTime.UtcNow; - await _userManager.UpdateAsync(user); + await _redisCacheService.RemoveCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); return await ServiceResult.Success(true); } + + public async Task> Logout(int userId, string refreshToken) + { + var result = await RevokeRefreshTokenAsync(userId, refreshToken); + if (!result.success) + { + return result; + } + await RevokeJwtToken(userId); + return await ServiceResult.Success(true); + } + private async Task> GetAuthToken(ApplicationUser user) { - var token = await GenerateJwtToken(user); + // Generate New JWT Token + string newTokenId = Guid.NewGuid().ToString(); + var token = await GenerateJwtToken(user, newTokenId); if (string.IsNullOrEmpty(token)) { var errors = new List { "Invalid Data" }; return await ServiceResult.Failure(errors); } - - RefreshToken refreshToken = null; - if (user.RefreshTokens !=null && user.RefreshTokens.Any(r => r.IsActive)) + var oldJwtId = await _redisCacheService.GetCacheAsync(AppConstants.UserJwtCacheKey(user.Id)); + if (!string.IsNullOrEmpty(oldJwtId)) { - refreshToken = user.RefreshTokens.FirstOrDefault(r => r.IsActive); + await _redisCacheService.RemoveCacheAsync(AppConstants.JwtCacheKey(oldJwtId)); + await _redisCacheService.RemoveCacheAsync(AppConstants.UserJwtCacheKey(user.Id)); } - else + + await _redisCacheService.SetCacheAsync(AppConstants.JwtCacheKey(newTokenId), user.Id, TimeSpan.FromMinutes(_jwtOptions.AccessExpiration)); + await _redisCacheService.SetCacheAsync(AppConstants.UserJwtCacheKey(user.Id),newTokenId, TimeSpan.FromMinutes(_jwtOptions.AccessExpiration)); + + //---------------------------------- + // Use the exist refreshToken or regenerate one + + var refreshToken = await _redisCacheService.GetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); + if (refreshToken == null) { refreshToken = await generateRefreshToken(); - user.RefreshTokens ??= new List(); - user.RefreshTokens.Add(refreshToken); + await _redisCacheService.SetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id), + refreshToken, TimeSpan.FromDays(refreshToken.ExpireAfter)); } - user.LastSeen = DateTime.UtcNow; - await _userManager.UpdateAsync(user); - return await ServiceResult.Success(new AuthResponseDTO() { Token = token, @@ -220,7 +230,7 @@ private async Task> GetAuthToken(ApplicationUser } }); } - private async Task generateRefreshToken() + private async Task generateRefreshToken() { var randomNumber = new byte[32]; @@ -228,15 +238,16 @@ private async Task generateRefreshToken() generator.GetBytes(randomNumber); - return new RefreshToken + return new RefreshTokenDto { Token = Convert.ToBase64String(randomNumber), - ExpireOn = DateTime.UtcNow.AddDays(10), - CreatedOn = DateTime.UtcNow + ExpireOn = DateTime.UtcNow.AddDays(_configuration.GetValue("ExpireTimes:Refresh_Token_Exp")), + CreatedOn = DateTime.UtcNow, + ExpireAfter = _configuration.GetValue("ExpireTimes:Refresh_Token_Exp") }; } - private Task GenerateJwtToken(ApplicationUser appUser) + private Task GenerateJwtToken(ApplicationUser appUser, string jti) { var tokenHandler = new JwtSecurityTokenHandler(); var tokenDescriptor = new SecurityTokenDescriptor(){ @@ -248,12 +259,22 @@ private Task GenerateJwtToken(ApplicationUser appUser) { new(ClaimTypes.Name, appUser.Name), new(ClaimTypes.Email, appUser.Email), - new("UserId", appUser.Id.ToString()) + new("UserId", appUser.Id.ToString()), + new("jti",jti) }) }; var securityToken = tokenHandler.CreateToken(tokenDescriptor); var accessToken = tokenHandler.WriteToken(securityToken); return Task.FromResult(accessToken); } + + private async Task RevokeJwtToken(int userId) + { + var oldJwtId = await _redisCacheService.GetCacheAsync(AppConstants.UserJwtCacheKey(userId)); + if (string.IsNullOrEmpty(oldJwtId)) return; + await _redisCacheService.RemoveCacheAsync(AppConstants.JwtCacheKey(oldJwtId)); + await _redisCacheService.RemoveCacheAsync(AppConstants.UserJwtCacheKey(userId)); + + } } diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs index 3e6fa04..1002074 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs @@ -10,4 +10,5 @@ public interface IAuthService public Task> RegisterAsync(RegisterUserDTO request); Task> RefreshTokenAsync(int id, string refreshToken); public Task> RevokeRefreshTokenAsync(int id, string refreshToken); + public Task> Logout(int userId, string refreshToken); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs index 6d5e48f..0782b19 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs @@ -2,9 +2,9 @@ namespace AskFm.BLL.Services.UserIdentityService; public class JwtOptions { - public string Issuer { get; set; } - public string Audience { get; set; } - public string SigningKey { get; set; } + public string? Issuer { get; set; } + public string? Audience { get; set; } + public string? SigningKey { get; set; } public int AccessExpiration { get; set; } // Minutes public int AccessRefreshTokenExpiration { get; set; } // days } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/ApplicationUser.cs b/AskFm/AskFm.DAL/Models/ApplicationUser.cs index 08f0869..5018f08 100644 --- a/AskFm/AskFm.DAL/Models/ApplicationUser.cs +++ b/AskFm/AskFm.DAL/Models/ApplicationUser.cs @@ -31,5 +31,5 @@ public class ApplicationUser : IdentityUser, ITrackable // tokens - public virtual ICollection? RefreshTokens { get; set; } + // public virtual ICollection? RefreshTokens { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/RefreshToken.cs b/AskFm/AskFm.DAL/Models/RefreshToken.cs index d1bbc9f..cc55bc6 100644 --- a/AskFm/AskFm.DAL/Models/RefreshToken.cs +++ b/AskFm/AskFm.DAL/Models/RefreshToken.cs @@ -1,14 +1,14 @@ -using Microsoft.EntityFrameworkCore; - -namespace AskFm.DAL.Models; -[Owned] -public class RefreshToken -{ - public string Token { get; set; } - public DateTime ExpireOn { get; set; } - public bool IsExpired => DateTime.Now >= ExpireOn; - public DateTime? RevokedOn { get; set; } - public bool IsActive => RevokedOn == null && !IsExpired; - - public DateTime CreatedOn { get; set; } -} \ No newline at end of file +// using Microsoft.EntityFrameworkCore; +// +// namespace AskFm.DAL.Models; +// // [Owned] +// public class RefreshToken +// { +// public string Token { get; set; } +// public DateTime ExpireOn { get; set; } +// public bool IsExpired => DateTime.Now >= ExpireOn; +// public DateTime? RevokedOn { get; set; } +// public bool IsActive => RevokedOn == null && !IsExpired; +// +// public DateTime CreatedOn { get; set; } +// } \ No newline at end of file diff --git a/AskFm/Shared/AppConstants.cs b/AskFm/Shared/AppConstants.cs new file mode 100644 index 0000000..fb1c834 --- /dev/null +++ b/AskFm/Shared/AppConstants.cs @@ -0,0 +1,20 @@ +namespace Shared; + +public class AppConstants +{ + + public static string UserJwtCacheKey(int userId) + { + return $"userId:{userId}:current_jti"; + } + + public static string JwtCacheKey(string id) + { + return $"jti:{id}"; + } + + public static string UserRefreshTokenCacheKey(int userId) + { + return $"refresh_token:userId:{userId}"; + } +} \ No newline at end of file From 3ab007f41d7a43885a5c6dc1ca84b4d28ea042e5 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Mon, 15 Sep 2025 09:40:19 +0300 Subject: [PATCH 81/92] Feat: reset password and configure email sender service --- AskFm/AskFm.API/Controllers/AuthController.cs | 34 +++++++ AskFm/AskFm.API/Program.cs | 26 +++-- AskFm/AskFm.API/appsettings.json | 7 ++ AskFm/AskFm.BLL/AskFm.BLL.csproj | 1 + AskFm/AskFm.BLL/DTO/EmailSettings.cs | 10 ++ .../DTO/UserDTOs/ForgotPasswordDto.cs | 10 ++ .../DTO/UserDTOs/ResetPasswordDto.cs | 20 ++++ AskFm/AskFm.BLL/Services/EmailSender.cs | 98 +++++++++++++++++++ AskFm/AskFm.BLL/Services/IEmailSender.cs | 12 +++ .../UserIdentityService/AuthService.cs | 84 +++++++++++----- .../UserIdentityService/IAuthService.cs | 3 +- .../UserIdentityService/IUserService.cs | 8 +- 12 files changed, 275 insertions(+), 38 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/EmailSettings.cs create mode 100644 AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs create mode 100644 AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs create mode 100644 AskFm/AskFm.BLL/Services/EmailSender.cs create mode 100644 AskFm/AskFm.BLL/Services/IEmailSender.cs diff --git a/AskFm/AskFm.API/Controllers/AuthController.cs b/AskFm/AskFm.API/Controllers/AuthController.cs index 1e06897..501bd3b 100644 --- a/AskFm/AskFm.API/Controllers/AuthController.cs +++ b/AskFm/AskFm.API/Controllers/AuthController.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using AskFm.BLL.DTO.UserDTOs; using AskFm.BLL.Services; using AskFm.BLL.Services.UserIdentityService; @@ -102,7 +103,40 @@ public async Task Logout(int id) return Ok(result); } + + [HttpPost] + [AllowAnonymous] + [Route("forgot-password")] + public async Task ForgotPassword(ForgotPasswordDto forgotPasswordDto) + { + + var result = await _authService.ForgotPasswordAsync(forgotPasswordDto.Email); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok("Check Your Email"); + } + [HttpPost] + [AllowAnonymous] + [Route("reset-password")] + public async Task ResetPassword(ResetPasswordDto resetPasswordDto) + { + if (resetPasswordDto == null) + { + return BadRequest("Invalid Data"); + } + + var result = await _authService.ResetPasswordAsync(resetPasswordDto); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok("Password Reset Success"); + } private void setRefreshToken(string refreshToken,DateTime expires) { var cookieOption = new CookieOptions() diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index b60f897..36d142c 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -14,7 +14,9 @@ using Microsoft.OpenApi.Models; using AskFm.BLL.Services.UserIdentityService; using Castle.Components.DictionaryAdapter.Xml; +using Microsoft.AspNetCore.Identity.UI.Services; using Shared; +using IEmailSender = AskFm.BLL.Services.IEmailSender; namespace AskFm.API; @@ -28,7 +30,6 @@ public static void Main(string[] args) // Add services to the container. builder.Services.AddControllers(); - // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); Env.Load(); @@ -37,19 +38,25 @@ public static void Main(string[] args) { throw new Exception("Connection string is null"); } + + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddHttpContextAccessor(); + // DbContext builder.Services.AddDbContext(options => options .UseLazyLoadingProxies() .UseSqlServer(ConnectionString)); - + // ------------------------------------------------- + // Register the repositories and services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddControllers(); - builder.Services.AddEndpointsApiExplorer(); + + // Configure Swagger with JWT Authentication builder.Services.AddSwaggerGen(setup => { var jwtSecurityScheme = new OpenApiSecurityScheme @@ -78,6 +85,8 @@ public static void Main(string[] args) }); + // Authentication & Authorization + JwtOptions jwtOptions = new JwtOptions { Issuer = Environment.GetEnvironmentVariable("ISSUER"), @@ -99,7 +108,8 @@ public static void Main(string[] args) Options.AccessExpiration = builder.Configuration.GetValue("ExpireTimes:Jwt_Token_Exp"); Options.AccessRefreshTokenExpiration = builder.Configuration.GetValue("ExpireTimes:Refresh_Token_Exp"); }); - + builder.Services.Configure(options => options.TokenLifespan = TimeSpan.FromHours(2)); + builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "Bearer"; @@ -138,6 +148,9 @@ public static void Main(string[] args) }); builder.Services.AddAuthorization(); + + + // Identity builder.Services.AddIdentity>(options => { //password configuration @@ -167,7 +180,7 @@ public static void Main(string[] args) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - + // Redis Cache builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration.GetConnectionString("Redis"); @@ -175,8 +188,7 @@ public static void Main(string[] args) }); builder.Services.AddSingleton(); - - + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/AskFm/AskFm.API/appsettings.json b/AskFm/AskFm.API/appsettings.json index 177edc2..7749d53 100644 --- a/AskFm/AskFm.API/appsettings.json +++ b/AskFm/AskFm.API/appsettings.json @@ -13,5 +13,12 @@ "Jwt_Token_Exp":10, "Refresh_Token_Exp":30 }, + "EmailOption": { + "client": "smtp.gmail.com", + "password": "password", + "from": "email", + "port":567 + }, + "ClientUrYour App Namel": " http://localhost:5180", "AllowedHosts": "*" } diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index 09d480a..766d1c2 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -14,6 +14,7 @@ + diff --git a/AskFm/AskFm.BLL/DTO/EmailSettings.cs b/AskFm/AskFm.BLL/DTO/EmailSettings.cs new file mode 100644 index 0000000..9582ff9 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/EmailSettings.cs @@ -0,0 +1,10 @@ +namespace AskFm.BLL.DTO; + +public class EmailSettings +{ + public string From {get; set;} + public string Client {get; set;} + public string Password {get;set;} + public int Port {get; set; } + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs new file mode 100644 index 0000000..c3de86b --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace AskFm.BLL.DTO.UserDTOs; + +public class ForgotPasswordDto +{ + [Required] + [EmailAddress] + public string Email { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs new file mode 100644 index 0000000..aa0b93b --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace AskFm.BLL.DTO.UserDTOs; + +public class ResetPasswordDto +{ + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + public string Token { get; set; } + + [Required] + public string NewPassword { get; set; } + + [Required] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/EmailSender.cs b/AskFm/AskFm.BLL/Services/EmailSender.cs new file mode 100644 index 0000000..d4bf40d --- /dev/null +++ b/AskFm/AskFm.BLL/Services/EmailSender.cs @@ -0,0 +1,98 @@ +using AskFm.DAL.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Net.Mail; +using AskFm.BLL.DTO; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.Extensions.Options; +using SendGrid; +using SendGrid.Helpers.Mail; + +namespace AskFm.BLL.Services; + +public class EmailSender : IEmailSender +{ + private readonly IConfiguration _config; + private readonly EmailSettings _emailSettings; + + // Inject IConfiguration and ILogger via the constructor + public EmailSender(IConfiguration config) + { + _config = config; + _emailSettings = new EmailSettings() + { + From = _config.GetValue("EmailOption:from"), + Client = _config.GetValue("EmailOption:client"), + Password = _config.GetValue("EmailOption:password"), + Port = _config.GetValue("EmailOption:port"), + }; + } + + public async Task> SendConfirmationLinkAsync(string email, string confirmationLink) + { + string subject = "Confirm Your Email for AskFm"; + string body = $@" +

Welcome to AskFm!

+

Thanks for registering. Please confirm your email address by clicking the link below:

+

Confirm My Email

+

If you did not create an account, you can safely ignore this email.

+
+

Thank you,

+

The AskFm Team

"; + + return await SendEmailAsync(email, subject, body); + } + + + public async Task> SendPasswordResetLinkAsync(string email, string resetLink) + { + string subject = "Reset Your AskFm Password"; + string body = $@" +

Password Reset Request

+

We received a request to reset your password. You can reset your password by clicking the link below:

+

Reset My Password

+

If you did not request a password reset, please ignore this email.

+
+

Thank you,

+

The AskFm Team

"; + + return await SendEmailAsync(email, subject, body); + } + + public async Task> SendPasswordResetCodeAsync( string email, string resetCode) + { + string subject = "Your AskFm Password Reset Code"; + string body = $@" +

Password Reset Code

+

We received a request to reset your password. Use the following code to complete the process:

+

{resetCode}

+

This code will expire shortly. If you did not request a password reset, please ignore this email.

+
+

Thank you,

+

The AskFm Team

"; + + return await SendEmailAsync(email, subject, body); + } + + + public async Task> SendEmailAsync(string toEmail, string subject, string htmlMessage) + { + + var client = new SmtpClient(_emailSettings.Client, 587) + { + EnableSsl = true, + Credentials = new NetworkCredential(_emailSettings.From, _emailSettings.Password) + }; + + // Create and send the email + await client.SendMailAsync( + new MailMessage(from: _emailSettings.From, + to: toEmail, + subject, + htmlMessage + )); + return await ServiceResult.Success(true); + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IEmailSender.cs b/AskFm/AskFm.BLL/Services/IEmailSender.cs new file mode 100644 index 0000000..432f1df --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IEmailSender.cs @@ -0,0 +1,12 @@ +using AskFm.DAL.Models; +using Microsoft.AspNetCore.Identity; + +namespace AskFm.BLL.Services; + +public interface IEmailSender +{ + public Task> SendPasswordResetLinkAsync( string email, string resetLink); + public Task> SendConfirmationLinkAsync(string email, string confirmationLink); + public Task> SendPasswordResetCodeAsync( string email, string resetCode); + Task> SendEmailAsync (string email, string subject, string message); +} diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs index 8662d25..e7a78c5 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs @@ -10,6 +10,7 @@ using Azure; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; @@ -24,13 +25,15 @@ public class AuthService : IAuthService private readonly JwtOptions _jwtOptions; private readonly RedisCacheService _redisCacheService; private readonly IConfiguration _configuration; + private readonly IEmailSender _emailSender; - public AuthService(IUnitOfWork unitOfWork, UserManager userManager, IOptions jwtOptions, RedisCacheService redisCacheService, IConfiguration configuration) + public AuthService(IUnitOfWork unitOfWork, UserManager userManager, IOptions jwtOptions, RedisCacheService redisCacheService, IConfiguration configuration, IEmailSender emailSender) { _unitOfWork = unitOfWork; _userManager = userManager; _redisCacheService = redisCacheService; _configuration = configuration; + _emailSender = emailSender; _jwtOptions = jwtOptions.Value; } @@ -46,7 +49,7 @@ public async Task> LoginAsync(LoginDTO request) } var getUser = await _userManager.FindByEmailAsync(request.Email); - + if (getUser == null) { @@ -77,21 +80,21 @@ public async Task> LoginAsync(LoginDTO request) return response; } - + public async Task> RegisterAsync(RegisterUserDTO request) { if (request == null) { - var errors = new List{ "Invalid Request Data" }; + var errors = new List { "Invalid Request Data" }; return await ServiceResult.Failure(errors); } var oldUser = _userManager.FindByEmailAsync(request.Email).Result; if (oldUser != null && oldUser.IsDeleted) { - var errors = new List{ "Email already exist" }; + var errors = new List { "Email already exist" }; return await ServiceResult.Failure(errors); } - + var newUser = new ApplicationUser() { Name = request.Name, @@ -101,16 +104,16 @@ public async Task> RegisterAsync(RegisterUserDTO AvatarPath = request.AvatarPath, LastSeen = DateTime.UtcNow }; - var createRsult = await _userManager.CreateAsync(newUser,request.Passwrod); - + var createRsult = await _userManager.CreateAsync(newUser, request.Passwrod); + if (createRsult.Succeeded == false) { - + var errors = createRsult.Errors.Select(e => e.Description).ToList(); return await ServiceResult.Failure(errors); - - + + } var response = await GetAuthToken(newUser); @@ -133,8 +136,8 @@ public async Task> RefreshTokenAsync(int id, stri } var oldRefreshToken = await _redisCacheService.GetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); - if (oldRefreshToken == null - || oldRefreshToken.IsExpired + if (oldRefreshToken == null + || oldRefreshToken.IsExpired || oldRefreshToken.Token != refreshToken) { var errors = new List { "Invalid Token." }; @@ -144,7 +147,7 @@ public async Task> RefreshTokenAsync(int id, stri return await GetAuthToken(user); } - + public async Task> RevokeRefreshTokenAsync(int id, string refreshToken) { if (string.IsNullOrEmpty(refreshToken)) @@ -159,7 +162,7 @@ public async Task> RevokeRefreshTokenAsync(int id, string re var errors = new List { "Invalid User." }; return await ServiceResult.Failure(errors); } - + var userRefreshToken = await _redisCacheService.GetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); if (userRefreshToken == null || userRefreshToken.IsExpired) { @@ -183,6 +186,39 @@ public async Task> Logout(int userId, string refreshToken) return await ServiceResult.Success(true); } + public async Task> ForgotPasswordAsync(string email) + { + var user = await _userManager.FindByEmailAsync(email); + if (user == null) + { + var error = new List {"Invalid Email."}; + return await ServiceResult.Failure(error); + } + var token = await _userManager.GeneratePasswordResetTokenAsync(user); + var resetUrl = + $"{_configuration.GetValue("ClientUrl")}/app/Auth/reset-password?email={email}&token={token}"; + + _emailSender.SendEmailAsync(email, "AskFm: Reset Password", resetUrl); + + return await ServiceResult.Success(true); + } + + public async Task> ResetPasswordAsync(ResetPasswordDto resetPasswordDto) + { + var user = await _userManager.FindByEmailAsync(resetPasswordDto.Email); + if (user == null) + { + var error = new List { "Invalid Data" }; + return await ServiceResult.Failure(error); + } + var result = await _userManager.ResetPasswordAsync(user, resetPasswordDto.Token, resetPasswordDto.NewPassword); + if (result.Succeeded) + { + return await ServiceResult.Success(true); + } + return await ServiceResult.Failure(result.Errors.Select(e => e.Description).ToList()); + } + private async Task> GetAuthToken(ApplicationUser user) { // Generate New JWT Token @@ -199,13 +235,13 @@ private async Task> GetAuthToken(ApplicationUser await _redisCacheService.RemoveCacheAsync(AppConstants.JwtCacheKey(oldJwtId)); await _redisCacheService.RemoveCacheAsync(AppConstants.UserJwtCacheKey(user.Id)); } - + await _redisCacheService.SetCacheAsync(AppConstants.JwtCacheKey(newTokenId), user.Id, TimeSpan.FromMinutes(_jwtOptions.AccessExpiration)); - await _redisCacheService.SetCacheAsync(AppConstants.UserJwtCacheKey(user.Id),newTokenId, TimeSpan.FromMinutes(_jwtOptions.AccessExpiration)); - + await _redisCacheService.SetCacheAsync(AppConstants.UserJwtCacheKey(user.Id), newTokenId, TimeSpan.FromMinutes(_jwtOptions.AccessExpiration)); + //---------------------------------- // Use the exist refreshToken or regenerate one - + var refreshToken = await _redisCacheService.GetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); if (refreshToken == null) { @@ -213,7 +249,7 @@ private async Task> GetAuthToken(ApplicationUser await _redisCacheService.SetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id), refreshToken, TimeSpan.FromDays(refreshToken.ExpireAfter)); } - + return await ServiceResult.Success(new AuthResponseDTO() { Token = token, @@ -250,12 +286,13 @@ private async Task generateRefreshToken() private Task GenerateJwtToken(ApplicationUser appUser, string jti) { var tokenHandler = new JwtSecurityTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor(){ + var tokenDescriptor = new SecurityTokenDescriptor() + { Issuer = _jwtOptions.Issuer, Audience = _jwtOptions.Audience, Expires = DateTime.UtcNow.AddMinutes(_jwtOptions.AccessExpiration), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SigningKey)), SecurityAlgorithms.HmacSha256), - Subject = new ClaimsIdentity(new Claim[] + Subject = new ClaimsIdentity(new Claim[] { new(ClaimTypes.Name, appUser.Name), new(ClaimTypes.Email, appUser.Email), @@ -274,7 +311,8 @@ private async Task RevokeJwtToken(int userId) if (string.IsNullOrEmpty(oldJwtId)) return; await _redisCacheService.RemoveCacheAsync(AppConstants.JwtCacheKey(oldJwtId)); await _redisCacheService.RemoveCacheAsync(AppConstants.UserJwtCacheKey(userId)); - + } + } diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs index 1002074..fe1cda3 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs @@ -6,9 +6,10 @@ namespace AskFm.BLL.Services.UserIdentityService; public interface IAuthService { public Task> LoginAsync(LoginDTO request); - // public Task> ResetPasswordAsync(string Email); public Task> RegisterAsync(RegisterUserDTO request); Task> RefreshTokenAsync(int id, string refreshToken); public Task> RevokeRefreshTokenAsync(int id, string refreshToken); public Task> Logout(int userId, string refreshToken); + public Task> ForgotPasswordAsync(string email); + public Task> ResetPasswordAsync(ResetPasswordDto resetPasswordDto); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs index 8a00ba2..ba4f9bf 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -14,17 +14,11 @@ public interface IUserService Task> GetCurrentUserAsync(); Task> UpdatePassword(int userId, UpdatePasswordDTO updatePasswordDto); Task> ResetEmail(int userId, string updatedEmail); - Task> ConfirmEmail(); /* GET Users only for now - getUserbyId - EditUser - DeleteUser - FollowUser - unfollowUser - reset password + confirm email Helper Function: getCurrentUserId */ From 8dc8e9f2d61b09370cd23b6ce7ccddaba0276c66 Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Mon, 15 Sep 2025 10:42:16 +0300 Subject: [PATCH 82/92] remove refresh tokens form databasse --- ...15073028_remove_refersh_tokens.Designer.cs | 810 ++++++++++++++++++ .../20250915073028_remove_refersh_tokens.cs | 45 + .../Migrations/AppDbContextModelSnapshot.cs | 37 - AskFm/AskFm.DAL/Models/ApplicationUser.cs | 3 - AskFm/AskFm.DAL/Models/RefreshToken.cs | 14 - 5 files changed, 855 insertions(+), 54 deletions(-) create mode 100644 AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.cs delete mode 100644 AskFm/AskFm.DAL/Models/RefreshToken.cs diff --git a/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.Designer.cs new file mode 100644 index 0000000..e1d8a3f --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.Designer.cs @@ -0,0 +1,810 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250915073028_remove_refersh_tokens")] + partial class remove_refersh_tokens + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserName") + .IsRequired() + .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.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(true); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("isRead") + .HasColumnType("bit"); + + b.Property("jsonContent") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.cs b/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.cs new file mode 100644 index 0000000..c3f3fc8 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250915073028_remove_refersh_tokens.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class remove_refersh_tokens : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RefreshToken"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RefreshToken", + columns: table => new + { + ApplicationUserId = table.Column(type: "int", nullable: false), + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + CreatedOn = table.Column(type: "datetime2", nullable: false), + ExpireOn = table.Column(type: "datetime2", nullable: false), + RevokedOn = table.Column(type: "datetime2", nullable: true), + Token = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshToken", x => new { x.ApplicationUserId, x.Id }); + table.ForeignKey( + name: "FK_RefreshToken_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index 832c22c..870e36f 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -584,43 +584,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => - { - b.OwnsMany("AskFm.DAL.Models.RefreshToken", "RefreshTokens", b1 => - { - b1.Property("ApplicationUserId") - .HasColumnType("int"); - - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); - - b1.Property("CreatedOn") - .HasColumnType("datetime2"); - - b1.Property("ExpireOn") - .HasColumnType("datetime2"); - - b1.Property("RevokedOn") - .HasColumnType("datetime2"); - - b1.Property("Token") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b1.HasKey("ApplicationUserId", "Id"); - - b1.ToTable("RefreshToken"); - - b1.WithOwner() - .HasForeignKey("ApplicationUserId"); - }); - - b.Navigation("RefreshTokens"); - }); - modelBuilder.Entity("AskFm.DAL.Models.Comment", b => { b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") diff --git a/AskFm/AskFm.DAL/Models/ApplicationUser.cs b/AskFm/AskFm.DAL/Models/ApplicationUser.cs index 5018f08..2b7655c 100644 --- a/AskFm/AskFm.DAL/Models/ApplicationUser.cs +++ b/AskFm/AskFm.DAL/Models/ApplicationUser.cs @@ -29,7 +29,4 @@ public class ApplicationUser : IdentityUser, ITrackable public DateTime UpdatedAt { get; set; } public DateTime CreatedAt { get; set; } - - // tokens - // public virtual ICollection? RefreshTokens { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/RefreshToken.cs b/AskFm/AskFm.DAL/Models/RefreshToken.cs deleted file mode 100644 index cc55bc6..0000000 --- a/AskFm/AskFm.DAL/Models/RefreshToken.cs +++ /dev/null @@ -1,14 +0,0 @@ -// using Microsoft.EntityFrameworkCore; -// -// namespace AskFm.DAL.Models; -// // [Owned] -// public class RefreshToken -// { -// public string Token { get; set; } -// public DateTime ExpireOn { get; set; } -// public bool IsExpired => DateTime.Now >= ExpireOn; -// public DateTime? RevokedOn { get; set; } -// public bool IsActive => RevokedOn == null && !IsExpired; -// -// public DateTime CreatedOn { get; set; } -// } \ No newline at end of file From ef94e309bdc6d244841b1dcf3befa6ec64edb914 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 21 Sep 2025 01:12:51 +0300 Subject: [PATCH 83/92] Apply ServiceResult in Notifications Service --- AskFm/AskFm.API/AskFm.API.csproj | 2 +- .../Controllers/NotificationController.cs | 9 +- .../DTO/CreateNotificationRequest.cs | 16 + .../Services/INotificationService.cs | 10 +- .../AskFm.BLL/Services/NotificationService.cs | 274 ++++++++++-------- AskFm/Tests/NotificationServiceTests.cs | 106 ++++--- 6 files changed, 247 insertions(+), 170 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/CreateNotificationRequest.cs diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index 2f95f72..60af690 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -23,7 +23,7 @@ - + diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs index 9271ecd..fbd551a 100644 --- a/AskFm/AskFm.API/Controllers/NotificationController.cs +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -1,3 +1,4 @@ +using AskFm.BLL.DTO; using AskFm.BLL.Services; using AskFm.DAL.Enums; using Microsoft.AspNetCore.Authorization; @@ -114,12 +115,4 @@ private int GetCurrentUserId() return userId; } } - - public class CreateNotificationRequest - { - public int UserId { get; set; } - public NotificationStatus Type { get; set; } - public int ResourceId { get; set; } - public string Message { get; set; } - } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CreateNotificationRequest.cs b/AskFm/AskFm.BLL/DTO/CreateNotificationRequest.cs new file mode 100644 index 0000000..1cd7eb2 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CreateNotificationRequest.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AskFm.DAL.Enums; + +namespace AskFm.BLL.DTO +{ + public class CreateNotificationRequest + { + public int UserId { get; set; } + public NotificationStatus Type { get; set; } + public int ResourceId { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs index 6f75ea9..afa49ad 100644 --- a/AskFm/AskFm.BLL/Services/INotificationService.cs +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -5,9 +5,9 @@ namespace AskFm.BLL.Services; public interface INotificationService { - Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); - Task> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); - Task MarkNotificationAsRead(int notificationId, int userId); - Task MarkAllNotificationsAsRead(int userId); - Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message); + Task>> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); + Task>> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); + Task> MarkNotificationAsRead(int notificationId, int userId); + Task> MarkAllNotificationsAsRead(int userId); + Task> CreateNotification(int userId, NotificationStatus type, int resourceId, string message); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 598f686..cd3c90b 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -1,18 +1,18 @@ using AskFm.BLL.DTO; using AskFm.BLL.Hub; -using AskFm.BLL.Services; using AskFm.DAL.Enums; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; using Microsoft.AspNetCore.SignalR; +namespace AskFm.BLL.Services; + public class NotificationService : INotificationService { private readonly INotificationRepository _notificationRepository; private readonly IUnitOfWork _unitOfWork; private readonly IHubContext _hubContext; - public NotificationService(INotificationRepository notificationRepository, IUnitOfWork unitOfWork, IHubContext hubContext) { _notificationRepository = notificationRepository; @@ -20,155 +20,191 @@ public NotificationService(INotificationRepository notificationRepository, IUnit _hubContext = hubContext; } - public async Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) + public async Task>> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) { - var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize); + try + { + var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize); - var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - var notificationDtos = new List(); + var notificationDtos = new List(); - foreach (var notification in notifications) - { - var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); - notificationDtos.Add(new NotificationDto + foreach (var notification in notifications) { - Id = notification.Id, - UserId = notification.UserId, - Type = notification.Type.ToString(), - ResourceId = notification.ResourceId, - Message = notification.Message, - IsRead = notification.IsRead, - CreatedAt = notification.CreatedAt, - Actor = actorUser == null ? null : new ActorDto - { - Id = actorUser.Id, - Username = actorUser?.UserName ?? "Unknown", - AvatarPath = actorUser?.AvatarPath ?? String.Empty - }, - Pagination = new PaginationDto + var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); + notificationDtos.Add(new NotificationDto { - CurrentPage = pageNumber, - TotalPages = totalPages, - TotalCount = totalCount, - HasNext = pageNumber < totalPages, - HasPrevious = pageNumber > 1 - } - }); + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.Message, + IsRead = notification.IsRead, + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser?.UserName ?? "Unknown", + AvatarPath = actorUser?.AvatarPath ?? String.Empty + }, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 + } + }); + } + return await ServiceResult>.Success(notificationDtos); + } + catch (Exception ex) + { + return await ServiceResult>.Failure(new List { ex.Message }); } - return notificationDtos; } - public async Task> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) + public async Task>> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) { - // Convert category to uppercase and match with enum - if (!Enum.TryParse(category.ToUpper(), out var notificationType)) - throw new ArgumentException($"Invalid notification category: {category}"); + try + { + // Convert category to uppercase and match with enum + if (!Enum.TryParse(category.ToUpper(), out var notificationType)) + return await ServiceResult>.Failure(new List { $"Invalid notification category: {category}" }); - var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); + var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); - var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - var notificationDtos = new List(); + var notificationDtos = new List(); - foreach (var notification in notifications) - { - var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); - notificationDtos.Add(new NotificationDto + foreach (var notification in notifications) { - Id = notification.Id, - UserId = notification.UserId, - Type = notification.Type.ToString(), - ResourceId = notification.ResourceId, - Message = notification.Message, - IsRead = notification.IsRead, - CreatedAt = notification.CreatedAt, - Actor = actorUser == null ? null : new ActorDto - { - Id = actorUser.Id, - Username = actorUser?.UserName ?? "Unknown", - AvatarPath = actorUser?.AvatarPath ?? String.Empty - }, - Pagination = new PaginationDto + var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); + notificationDtos.Add(new NotificationDto { - CurrentPage = pageNumber, - TotalPages = totalPages, - TotalCount = totalCount, - HasNext = pageNumber < totalPages, - HasPrevious = pageNumber > 1 - } - }); - } + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.Message, + IsRead = notification.IsRead, + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser?.UserName ?? "Unknown", + AvatarPath = actorUser?.AvatarPath ?? String.Empty + }, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 + } + }); + } - return notificationDtos; + return await ServiceResult>.Success(notificationDtos); + } + catch (Exception ex) + { + return await ServiceResult>.Failure(new List { ex.Message }); + } } - public async Task MarkNotificationAsRead(int notificationId, int userId) + + public async Task> MarkNotificationAsRead(int notificationId, int userId) { - var notification = await _notificationRepository.GetUserNotificationById(notificationId, userId); - if (notification == null) - throw new InvalidOperationException("Notification not found or access denied."); + try + { + var notification = await _notificationRepository.GetUserNotificationById(notificationId, userId); + if (notification == null) + return await ServiceResult.Failure(new List { "Notification not found or access denied." }); - notification.IsRead = true; - _unitOfWork.Notifications.Update(notification); - await _unitOfWork.SaveAsync(); + notification.IsRead = true; + _unitOfWork.Notifications.Update(notification); + await _unitOfWork.SaveAsync(); - return "notification has been read"; + return await ServiceResult.Success("notification has been read"); + } + catch (Exception ex) + { + return await ServiceResult.Failure(new List { ex.Message }); + } } - public async Task MarkAllNotificationsAsRead(int userId) + public async Task> MarkAllNotificationsAsRead(int userId) { - var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.IsRead); + try + { + var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.IsRead); - foreach (var notification in unreadNotifications) + foreach (var notification in unreadNotifications) + { + notification.IsRead = true; + _unitOfWork.Notifications.Update(notification); + } + + await _unitOfWork.SaveAsync(); + return await ServiceResult.Success("All notifications marked as read"); + } + catch (Exception ex) { - notification.IsRead = true; - _unitOfWork.Notifications.Update(notification); + return await ServiceResult.Failure(new List { ex.Message }); } - - await _unitOfWork.SaveAsync(); - return "All notifications marked as read"; } - public async Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message) + public async Task> CreateNotification(int userId, NotificationStatus type, int resourceId, string message) { - var notification = new Notification - { - UserId = userId, - Type = type, - ResourceId = resourceId, - Message = message, - IsRead = false, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - - await _unitOfWork.Notifications.AddAsync(notification); - await _unitOfWork.SaveAsync(); - - // Get actor information for the notification - var actorUser = await _notificationRepository.GetActorUserByResourceId(resourceId, type); - - var notificationDto = new NotificationDto + try { - Id = notification.Id, - UserId = notification.UserId, - Type = notification.Type.ToString(), - ResourceId = notification.ResourceId, - Message = notification.Message, - IsRead = notification.IsRead, - CreatedAt = notification.CreatedAt, - Actor = actorUser == null ? null : new ActorDto + var notification = new Notification { - Id = actorUser.Id, - Username = actorUser.UserName ?? "Unknown", - AvatarPath = actorUser.AvatarPath ?? string.Empty - } - }; + UserId = userId, + Type = type, + ResourceId = resourceId, + Message = message, + IsRead = false, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _unitOfWork.Notifications.AddAsync(notification); + await _unitOfWork.SaveAsync(); + + // Get actor information for the notification + var actorUser = await _notificationRepository.GetActorUserByResourceId(resourceId, type); + + var notificationDto = new NotificationDto + { + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.Message, + IsRead = notification.IsRead, + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser.UserName ?? "Unknown", + AvatarPath = actorUser.AvatarPath ?? string.Empty + } + }; - // Send real-time notification to the specific user - await _hubContext.Clients.Group($"user_{userId}") - .SendAsync("ReceiveNotification", notificationDto); + // Send real-time notification to the specific user + await _hubContext.Clients.Group($"user_{userId}") + .SendAsync("ReceiveNotification", notificationDto); + + return await ServiceResult.Success(notificationDto); + } + catch (Exception ex) + { + return await ServiceResult.Failure(new List { ex.Message }); + } } - - } \ No newline at end of file diff --git a/AskFm/Tests/NotificationServiceTests.cs b/AskFm/Tests/NotificationServiceTests.cs index dc830ce..9a701ef 100644 --- a/AskFm/Tests/NotificationServiceTests.cs +++ b/AskFm/Tests/NotificationServiceTests.cs @@ -76,15 +76,18 @@ public async Task GetUserNotifications_ReturnsCorrectDtoWithPagination() var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); // Assert - Assert.Single(result); - Assert.Equal(1, result[0].Id); - Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); - Assert.Equal("Test notification", result[0].Message); - Assert.False(result[0].IsRead); - Assert.Equal("test_user", result[0].Actor.Username); - Assert.Equal("test.jpg", result[0].Actor.AvatarPath); - Assert.Equal(2, result[0].Actor.Id); - Assert.Equal(1, result[0].Pagination.TotalCount); + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + Assert.Single(result.Data); + Assert.Equal(1, result.Data[0].Id); + Assert.Equal(NotificationStatus.QUESTION.ToString(), result.Data[0].Type); + Assert.Equal("Test notification", result.Data[0].Message); + Assert.False(result.Data[0].IsRead); + Assert.Equal("test_user", result.Data[0].Actor.Username); + Assert.Equal("test.jpg", result.Data[0].Actor.AvatarPath); + Assert.Equal(2, result.Data[0].Actor.Id); + Assert.Equal(1, result.Data[0].Pagination.TotalCount); } [Fact] @@ -117,13 +120,16 @@ public async Task GetUserNotifications_WithNullActor_ReturnsCorrectDtoWithPagina var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); // Assert - Assert.Single(result); - Assert.Equal(1, result[0].Id); - Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); - Assert.Equal("Test notification", result[0].Message); - Assert.False(result[0].IsRead); - Assert.Null(result[0].Actor); - Assert.Equal(1, result[0].Pagination.TotalCount); + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + Assert.Single(result.Data); + Assert.Equal(1, result.Data[0].Id); + Assert.Equal(NotificationStatus.QUESTION.ToString(), result.Data[0].Type); + Assert.Equal("Test notification", result.Data[0].Message); + Assert.False(result.Data[0].IsRead); + Assert.Null(result.Data[0].Actor); + Assert.Equal(1, result.Data[0].Pagination.TotalCount); } [Fact] @@ -158,19 +164,22 @@ public async Task GetNotificationsByType_WithValidCategory_ReturnsFilteredNotifi var result = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); // Assert - Assert.Single(result); - Assert.Equal(1, result[0].Id); - Assert.Equal(category.ToUpper(), result[0].Type); - Assert.Equal("Test notification", result[0].Message); - Assert.False(result[0].IsRead); - Assert.Equal("test_user", result[0].Actor.Username); - Assert.Equal("test.jpg", result[0].Actor.AvatarPath); - Assert.Equal(2, result[0].Actor.Id); - Assert.Equal(1, result[0].Pagination.TotalCount); + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + Assert.Single(result.Data); + Assert.Equal(1, result.Data[0].Id); + Assert.Equal(category.ToUpper(), result.Data[0].Type); + Assert.Equal("Test notification", result.Data[0].Message); + Assert.False(result.Data[0].IsRead); + Assert.Equal("test_user", result.Data[0].Actor.Username); + Assert.Equal("test.jpg", result.Data[0].Actor.AvatarPath); + Assert.Equal(2, result.Data[0].Actor.Id); + Assert.Equal(1, result.Data[0].Pagination.TotalCount); } [Fact] - public async Task GetNotificationsByType_WithInvalidCategory_ThrowsArgumentException() + public async Task GetNotificationsByType_WithInvalidCategory_ReturnsFailureResult() { // Arrange int userId = 1; @@ -178,9 +187,14 @@ public async Task GetNotificationsByType_WithInvalidCategory_ThrowsArgumentExcep int pageNumber = 1; int pageSize = 10; - // Act & Assert - var ex = await Assert.ThrowsAsync(() => _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize)); - Assert.Contains("Invalid notification category", ex.Message); + // Act + var result = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); + + // Assert + Assert.False(result.success); + Assert.NotNull(result.Errors); + Assert.Contains("Invalid notification category", result.Errors[0]); + Assert.Null(result.Data); } [Fact] @@ -209,14 +223,16 @@ public async Task MarkNotificationAsRead_WithValidId_UpdatesNotification() var result = await _notificationService.MarkNotificationAsRead(notificationId, userId); // Assert + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.Equal("notification has been read", result.Data); Assert.True(notification.IsRead); - Assert.Equal("notification has been read", result); _unitOfWorkMock.Verify(p => p.Notifications.Update(notification), Times.Once); _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); } [Fact] - public async Task MarkNotificationAsRead_WithInvalidId_ThrowsInvalidOperationException() + public async Task MarkNotificationAsRead_WithInvalidId_ReturnsFailureResult() { // Arrange var notificationId = 999; @@ -224,11 +240,14 @@ public async Task MarkNotificationAsRead_WithInvalidId_ThrowsInvalidOperationExc _notificationRepositoryMock.Setup(p => p.GetUserNotificationById(notificationId, userId)) .ReturnsAsync((Notification)null); - // Act & Assert - var ex = await Assert.ThrowsAsync( - () => _notificationService.MarkNotificationAsRead(notificationId, userId)); + // Act + var result = await _notificationService.MarkNotificationAsRead(notificationId, userId); - Assert.Contains("Notification not found or access denied", ex.Message); + // Assert + Assert.False(result.success); + Assert.NotNull(result.Errors); + Assert.Contains("Notification not found or access denied.", result.Errors[0]); + Assert.Null(result.Data); } [Fact] @@ -270,7 +289,9 @@ public async Task MarkAllNotificationsAsRead_UpdatesAllUnreadNotifications() var result = await _notificationService.MarkAllNotificationsAsRead(userId); // Assert - Assert.Equal("All notifications marked as read", result); + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.Equal("All notifications marked as read", result.Data); Assert.All(unreadNotifications, n => Assert.True(n.IsRead)); _unitOfWorkMock.Verify(p => p.Notifications.Update(It.IsAny()), Times.Exactly(2)); _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); @@ -292,9 +313,20 @@ public async Task CreateNotification_CreatesAndSendsNotification() .ReturnsAsync(actorUser); // Act - await _notificationService.CreateNotification(userId, type, resourceId, message); + var result = await _notificationService.CreateNotification(userId, type, resourceId, message); // Assert + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + Assert.Equal(userId, result.Data.UserId); + Assert.Equal(type.ToString(), result.Data.Type); + Assert.Equal(resourceId, result.Data.ResourceId); + Assert.Equal(message, result.Data.Message); + Assert.False(result.Data.IsRead); + Assert.Equal("test_user", result.Data.Actor.Username); + Assert.Equal("test.jpg", result.Data.Actor.AvatarPath); + Assert.Equal(2, result.Data.Actor.Id); _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.IsAny()), Times.Once); _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync("ReceiveNotification", It.IsAny(), default), Times.Once); From 97620e4bab13e11316730c635ad3e98e202f177d Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 21 Sep 2025 01:45:07 +0300 Subject: [PATCH 84/92] fix(auth): Use standard JWT NameIdentifier claim for user ID extraction --- AskFm/AskFm.API/Controllers/NotificationController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs index fbd551a..a7855da 100644 --- a/AskFm/AskFm.API/Controllers/NotificationController.cs +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -1,3 +1,4 @@ +using System.Security.Claims; using AskFm.BLL.DTO; using AskFm.BLL.Services; using AskFm.DAL.Enums; @@ -107,7 +108,8 @@ public async Task CreateNotification([FromBody] CreateNotificatio private int GetCurrentUserId() { - var userIdClaim = User.FindFirst("UserId")?.Value; + // Use the standard NameIdentifier claim + var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out int userId)) { throw new UnauthorizedAccessException("Invalid user token"); From 6be6860fa723faa2f4784aca9280162f12413fbf Mon Sep 17 00:00:00 2001 From: Task Grading Bot Date: Mon, 22 Sep 2025 21:49:08 +0300 Subject: [PATCH 85/92] Refactor: Validate in data in emailsettings dto --- AskFm/AskFm.BLL/DTO/EmailSettings.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AskFm/AskFm.BLL/DTO/EmailSettings.cs b/AskFm/AskFm.BLL/DTO/EmailSettings.cs index 9582ff9..a7953f3 100644 --- a/AskFm/AskFm.BLL/DTO/EmailSettings.cs +++ b/AskFm/AskFm.BLL/DTO/EmailSettings.cs @@ -1,10 +1,16 @@ +using System.ComponentModel.DataAnnotations; + namespace AskFm.BLL.DTO; public class EmailSettings { + [Required, EmailAddress] public string From {get; set;} + [Required] public string Client {get; set;} + [Required] public string Password {get;set;} + [Range(1, 65535)] public int Port {get; set; } } \ No newline at end of file From 050c57c93af62d49dd31aa8baa6a33d61376f17b Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Thu, 11 Sep 2025 00:59:06 +0300 Subject: [PATCH 86/92] Added CreateThread endpoint --- .../AskFm.API/Controllers/ThreadController.cs | 92 ++++++++++++ AskFm/AskFm.API/Program.cs | 1 + AskFm/AskFm.BLL/DTO/CreateThreadDto.cs | 16 +++ AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs | 25 ++++ AskFm/AskFm.BLL/Services/IThreadService.cs | 9 ++ AskFm/AskFm.BLL/Services/ThreadService.cs | 136 ++++++++++++++++++ 6 files changed, 279 insertions(+) create mode 100644 AskFm/AskFm.API/Controllers/ThreadController.cs create mode 100644 AskFm/AskFm.BLL/DTO/CreateThreadDto.cs create mode 100644 AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs create mode 100644 AskFm/AskFm.BLL/Services/IThreadService.cs create mode 100644 AskFm/AskFm.BLL/Services/ThreadService.cs diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs new file mode 100644 index 0000000..9202b09 --- /dev/null +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -0,0 +1,92 @@ +using AskFm.BLL.DTO; +using AskFm.BLL.Services; +using AskFm.BLL.Services.UserIdentityService; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; + +namespace AskFm.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(AuthenticationSchemes = "Bearer")] +public class ThreadController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IThreadService _threadService; + + public ThreadController( + ILogger logger, + IUserService userService, + IThreadService threadService) + { + _logger = logger; + _userService = userService; + _threadService = threadService; + } + + + // POST api/threads/ - Ask a question + [HttpPost] + [Route("thread")] + public async Task AskQuestion(CreateThreadDto createThreadDto) + { + // Asker Id -> Current User + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + var userId = user.Data.Id; + + var result = await _threadService.AddThread(userId, createThreadDto); + + if (!result.success) + { + return BadRequest(result.Errors); + } + return Ok(result.Data); + + } + + + [HttpGet] + [Route("thread/{id}")] + public async Task GetAllThreads([FromRoute] int id) + { + var threads = await _threadService.GetAllThreads(id); + if (!threads.success) + { + return BadRequest(threads.Errors); + } + return Ok(threads.Data); + } + + // GET api/threads/{id} - Getting the Thread with id = {id} + + [HttpGet] + [Route("thread/{id}")] + void GetThreadWithId([FromRoute] string id) + { + + } + + // PUT api/threads/{id}/answer - Add an Answer on the thread with id = {id} + [HttpPut] + [Route("threads/{id}/answer")] + void AnswerQuestion([FromRoute] string id) + { + + } + + // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} + // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} + // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + + + // POST api/threads/{id}/comments - Add a comment to the thread with id = {id} + // GET api/threads/{id}/comments - Get all the comments to the thread with id = {id} + // DELETE api/threads/{id}/comments - Remove the comment to the thread with id = {id} + +} \ No newline at end of file diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 9bf0d5a..bbcc284 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -56,6 +56,7 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs new file mode 100644 index 0000000..4b078ab --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs @@ -0,0 +1,16 @@ +using AskFm.DAL.Enums; +using AskFm.DAL.Models; + +namespace AskFm.BLL.DTO; + +public class CreateThreadDto +{ + public int AskedId { get; set; } + + public string QuestionContent { get; set; } + + public ThreadStatus Status { get; set; } + + public bool isAnonymous { get; set; } + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs new file mode 100644 index 0000000..92c3b63 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs @@ -0,0 +1,25 @@ +using AskFm.DAL.Enums; + +namespace AskFm.BLL.DTO; + +public class ThreadResponseDto +{ + public int Id { get; set; } + + public string QuestionContent { get; set; } + + public ThreadStatus Status { get; set; } + + public bool IsAnonymous { get; set; } + + public DateTime CreatedAt { get; set; } + + // Simplified user info instead of full objects + public int AskerId { get; set; } + + public string AskerName { get; set; } + + public int AskedId { get; set; } + + public string AskedName { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs new file mode 100644 index 0000000..c0a8d9f --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IThreadService.cs @@ -0,0 +1,9 @@ +using AskFm.BLL.DTO; +using Thread = AskFm.DAL.Models.Thread; + +namespace AskFm.BLL.Services; +public interface IThreadService +{ + Task> AddThread(int userId, CreateThreadDto createThreadDto); + Task>> GetAllThreads(int userId); +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs new file mode 100644 index 0000000..a8bec73 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -0,0 +1,136 @@ +using AskFm.BLL.DTO; +using AskFm.DAL.Interfaces; +using AutoMapper; +using Microsoft.Extensions.Logging; +using Thread = AskFm.DAL.Models.Thread; + +namespace AskFm.BLL.Services; + +public class ThreadService : IThreadService +{ + + private IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly IMapper _mapper; + public ThreadService(IUnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + public async Task> AddThread(int askerId, CreateThreadDto createThreadDto) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // check if the Asked user exite or not + var askedId = createThreadDto.AskedId; + + var askedUser = await _unitOfWork.Users.GetByIdAsync(askedId); + if (askedUser == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List(){"Could not find asked user"}); + } + + // getting the Asker user object + var askerUser = await _unitOfWork.Users.GetByIdAsync(askerId); + if (askerUser == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List(){"Could not find asker user"}); + } + + + + // check if the Asked User's id == Asker User's Id + if (askedId == askerId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List(){"User can not ask him self"}); + } + + + // check if the QuestionContent is empty or not + if (string.IsNullOrEmpty(createThreadDto.QuestionContent)) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List(){"Question Can't be null or empty"}); + } + + // creating a new thread + Thread thread = new Thread + { + AskedId = askedId, + + AskerId = askerId, + + QuestionContent = createThreadDto.QuestionContent, + + AnswerContent = "", + Status = createThreadDto.Status, + isAnonymous = createThreadDto.isAnonymous, + CreatedAt = DateTime.Now, + }; + + await _unitOfWork.Threads.AddAsync(thread); + askerUser.AskedThreads?.Add(thread); + askedUser.ReceivedThreads?.Add(thread); + await _unitOfWork.Users.UpdateAsync(askedUser); + await _unitOfWork.Users.UpdateAsync(askerUser); + await _unitOfWork.SaveAsync(); + + transaction.Commit(); + + var responseDto = new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId.Value, + AskerName = thread.Asker?.Name ?? "Unknown", + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name ?? "Unknown" + }; + + return await ServiceResult.Success(responseDto); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List(){e.Message}); + } + } + + public async Task>> GetAllThreads(int userId) + { + var user = await _unitOfWork.Users.FindAsync( + predicate: u => u.Id == userId, + includes: new[] { "ReceivedThreads" } + ); + + + if (user == null) + { + return await ServiceResult>.Failure(new List(){"Could not find user"}); + } + + var res = user.ReceivedThreads.Select(t => new ThreadResponseDto + { + Id = t.Id, + QuestionContent = t.QuestionContent, + IsAnonymous = t.isAnonymous, + CreatedAt = t.CreatedAt, + AskedId = t.AskedId, + Status = t.Status, + AskedName = t.Asked?.Name ?? "Unknown", + AskerId = t.AskerId.Value, + AskerName = t.Asker?.Name ?? "Unknown" + + }).ToList(); + + return await ServiceResult>.Success(res); + } +} \ No newline at end of file From d01a36a7eec3ef073e77b3a918da1b3ad9144050 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sat, 27 Sep 2025 08:22:56 +0300 Subject: [PATCH 87/92] Feat: Added the answer thread endpoint, created the ThreadLikeController --- .../AskFm.API/Controllers/ThreadController.cs | 6 +- .../Controllers/ThreadLikeController.cs | 34 +++++ AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs | 8 ++ AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs | 2 + AskFm/AskFm.BLL/Services/IThreadService.cs | 1 + AskFm/AskFm.BLL/Services/ThreadService.cs | 136 ++++++++++++------ 6 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 AskFm/AskFm.API/Controllers/ThreadLikeController.cs create mode 100644 AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs index 9202b09..24ab814 100644 --- a/AskFm/AskFm.API/Controllers/ThreadController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -29,7 +29,7 @@ public ThreadController( // POST api/threads/ - Ask a question [HttpPost] - [Route("thread")] + [Route("threads")] public async Task AskQuestion(CreateThreadDto createThreadDto) { // Asker Id -> Current User @@ -51,6 +51,7 @@ public async Task AskQuestion(CreateThreadDto createThreadDto) } + // get all threads for user by user id [HttpGet] [Route("thread/{id}")] public async Task GetAllThreads([FromRoute] int id) @@ -66,11 +67,12 @@ public async Task GetAllThreads([FromRoute] int id) // GET api/threads/{id} - Getting the Thread with id = {id} [HttpGet] - [Route("thread/{id}")] + [Route("threads/{id}")] void GetThreadWithId([FromRoute] string id) { } + // PUT api/threads/{id}/answer - Add an Answer on the thread with id = {id} [HttpPut] diff --git a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs new file mode 100644 index 0000000..eb59f73 --- /dev/null +++ b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs @@ -0,0 +1,34 @@ +using AskFm.BLL.Services; +using AskFm.BLL.Services.UserIdentityService; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace AskFm.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(AuthenticationSchemes = "Bearer")] +public class ThreadLikeController +{ + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IThreadService _threadService; + + public ThreadLikeController( + ILogger logger, + IUserService userService, + IThreadService threadService) + { + _logger = logger; + _userService = userService; + _threadService = threadService; + } + + + // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} + // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} + // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + + + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs b/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs new file mode 100644 index 0000000..16c3cfc --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs @@ -0,0 +1,8 @@ +namespace AskFm.BLL.DTO; + +public class ThreadAnswerDto +{ + public int threadId; + public string answer; + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs index 92c3b63..1a6775c 100644 --- a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs @@ -22,4 +22,6 @@ public class ThreadResponseDto public int AskedId { get; set; } public string AskedName { get; set; } + + public string answer; } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs index c0a8d9f..7f550fc 100644 --- a/AskFm/AskFm.BLL/Services/IThreadService.cs +++ b/AskFm/AskFm.BLL/Services/IThreadService.cs @@ -6,4 +6,5 @@ public interface IThreadService { Task> AddThread(int userId, CreateThreadDto createThreadDto); Task>> GetAllThreads(int userId); + Task> AnswerThread(ThreadAnswerDto threadAnswerDto); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs index a8bec73..080b0eb 100644 --- a/AskFm/AskFm.BLL/Services/ThreadService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -8,15 +8,16 @@ namespace AskFm.BLL.Services; public class ThreadService : IThreadService { - private IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly IMapper _mapper; - public ThreadService(IUnitOfWork unitOfWork, ILogger logger) + + public ThreadService(IUnitOfWork unitOfWork, ILogger logger) { _unitOfWork = unitOfWork; _logger = logger; } + public async Task> AddThread(int askerId, CreateThreadDto createThreadDto) { var transaction = await _unitOfWork.BeginTransactionAsync(); @@ -25,63 +26,66 @@ public async Task> AddThread(int askerId, Creat { // check if the Asked user exite or not var askedId = createThreadDto.AskedId; - + var askedUser = await _unitOfWork.Users.GetByIdAsync(askedId); if (askedUser == null) { await transaction.RollbackAsync(); - return await ServiceResult.Failure(new List(){"Could not find asked user"}); + return await ServiceResult.Failure( + new List() { "Could not find asked user" }); } - + // getting the Asker user object var askerUser = await _unitOfWork.Users.GetByIdAsync(askerId); if (askerUser == null) { await transaction.RollbackAsync(); - return await ServiceResult.Failure(new List(){"Could not find asker user"}); + return await ServiceResult.Failure( + new List() { "Could not find asker user" }); } - - + // check if the Asked User's id == Asker User's Id if (askedId == askerId) { await transaction.RollbackAsync(); - return await ServiceResult.Failure(new List(){"User can not ask him self"}); + return await ServiceResult.Failure( + new List() { "User can not ask him self" }); } - - + + // check if the QuestionContent is empty or not if (string.IsNullOrEmpty(createThreadDto.QuestionContent)) { await transaction.RollbackAsync(); - return await ServiceResult.Failure(new List(){"Question Can't be null or empty"}); + return await ServiceResult.Failure(new List() + { "Question Can't be null or empty" }); } - + // creating a new thread Thread thread = new Thread { AskedId = askedId, - + AskerId = askerId, QuestionContent = createThreadDto.QuestionContent, - + AnswerContent = "", Status = createThreadDto.Status, isAnonymous = createThreadDto.isAnonymous, CreatedAt = DateTime.Now, }; - + await _unitOfWork.Threads.AddAsync(thread); askerUser.AskedThreads?.Add(thread); askedUser.ReceivedThreads?.Add(thread); await _unitOfWork.Users.UpdateAsync(askedUser); await _unitOfWork.Users.UpdateAsync(askerUser); await _unitOfWork.SaveAsync(); - + transaction.Commit(); - + var responseDto = new ThreadResponseDto { Id = thread.Id, @@ -94,43 +98,91 @@ public async Task> AddThread(int askerId, Creat AskedId = thread.AskedId, AskedName = thread.Asked?.Name ?? "Unknown" }; - + return await ServiceResult.Success(responseDto); } catch (Exception e) { await transaction.RollbackAsync(); - return await ServiceResult.Failure(new List(){e.Message}); + return await ServiceResult.Failure(new List() { e.Message }); } } public async Task>> GetAllThreads(int userId) { - var user = await _unitOfWork.Users.FindAsync( - predicate: u => u.Id == userId, - includes: new[] { "ReceivedThreads" } - ); - - - if (user == null) + try + { + var user = await _unitOfWork.Users.FindAsync( + predicate: u => u.Id == userId, + includes: new[] { "ReceivedThreads" } + ); + + + if (user == null) + { + return await ServiceResult>.Failure( + new List() { "Could not find user" }); + } + + var res = user.ReceivedThreads.Select(t => new ThreadResponseDto + { + Id = t.Id, + QuestionContent = t.QuestionContent, + IsAnonymous = t.isAnonymous, + CreatedAt = t.CreatedAt, + AskedId = t.AskedId, + Status = t.Status, + AskedName = t.Asked?.Name ?? "Unknown", + AskerId = t.AskerId.Value, + AskerName = t.Asker?.Name ?? "Unknown" + }).ToList(); + + return await ServiceResult>.Success(res); + } + catch (Exception e) { - return await ServiceResult>.Failure(new List(){"Could not find user"}); + return await ServiceResult>.Failure(new List() { e.Message }); } + } + + public async Task> AnswerThread(ThreadAnswerDto threadAnswerDto) + { + try + { + // get thread + var threadId = threadAnswerDto.threadId; + var thread = await _unitOfWork.Threads.GetByIdAsync(threadId); + // chekc if thread + if (thread == null) + { + return await ServiceResult.Failure(new List() { "Could not find thread" }); + } + // put the answer on it + thread.AnswerContent = threadAnswerDto.answer; + // save changes + await _unitOfWork.Threads.UpdateAsync(thread); + await _unitOfWork.SaveAsync(); - var res = user.ReceivedThreads.Select(t => new ThreadResponseDto + ThreadResponseDto threadRes = new ThreadResponseDto() + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskedId = thread.AskedId, + AskedName = thread?.Asked.Name ?? "Unknown", + AskerId = thread.AskerId.Value, + AskerName = thread.isAnonymous? "Unknown" : thread?.Asker.Name, + answer = thread.AnswerContent, + Status = thread.Status, + }; + return await ServiceResult.Success(threadRes); + } + catch (Exception e) { - Id = t.Id, - QuestionContent = t.QuestionContent, - IsAnonymous = t.isAnonymous, - CreatedAt = t.CreatedAt, - AskedId = t.AskedId, - Status = t.Status, - AskedName = t.Asked?.Name ?? "Unknown", - AskerId = t.AskerId.Value, - AskerName = t.Asker?.Name ?? "Unknown" - - }).ToList(); - - return await ServiceResult>.Success(res); + return await ServiceResult.Failure(new List() { e.Message }); + } } + + } \ No newline at end of file From e71d3b388d762899b67d757f56e83cdbdfa4c3b5 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 6 Oct 2025 00:12:33 +0300 Subject: [PATCH 88/92] Finished the ThreadLikeService and endpoints --- .../AskFm.API/Controllers/ThreadController.cs | 3 +- .../Controllers/ThreadLikeController.cs | 62 +++++- AskFm/AskFm.API/Program.cs | 1 + AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs | 4 +- AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs | 12 ++ .../AskFm.BLL/Services/IThreadLikeService.cs | 11 + AskFm/AskFm.BLL/Services/ThreadLikeService.cs | 202 ++++++++++++++++++ AskFm/AskFm.BLL/Services/ThreadService.cs | 6 +- 8 files changed, 288 insertions(+), 13 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs create mode 100644 AskFm/AskFm.BLL/Services/IThreadLikeService.cs create mode 100644 AskFm/AskFm.BLL/Services/ThreadLikeService.cs diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs index 24ab814..3a9a539 100644 --- a/AskFm/AskFm.API/Controllers/ThreadController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -29,7 +29,7 @@ public ThreadController( // POST api/threads/ - Ask a question [HttpPost] - [Route("threads")] + [Route("thread")] public async Task AskQuestion(CreateThreadDto createThreadDto) { // Asker Id -> Current User @@ -83,6 +83,7 @@ void AnswerQuestion([FromRoute] string id) } // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} + // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} diff --git a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs index eb59f73..0ba9367 100644 --- a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs @@ -1,34 +1,82 @@ +using AskFm.BLL.DTO; using AskFm.BLL.Services; using AskFm.BLL.Services.UserIdentityService; -using Microsoft.AspNetCore.Authorization; +using AutoMapper; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; namespace AskFm.API.Controllers; [ApiController] [Route("api/[controller]")] [Authorize(AuthenticationSchemes = "Bearer")] -public class ThreadLikeController +public class ThreadLikeController : ControllerBase { private readonly ILogger _logger; private readonly IUserService _userService; - private readonly IThreadService _threadService; + private readonly IThreadLikeService _threadLikeService; public ThreadLikeController( ILogger logger, IUserService userService, - IThreadService threadService) + IThreadLikeService threadService) { _logger = logger; _userService = userService; - _threadService = threadService; + _threadLikeService = threadService; } // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} - // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} - // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + [HttpPost] + [Route("threads/{id}/likes")] + public async Task LikeThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + var res = await _threadLikeService.AddLike(id, user.Data.Id); + if (!res.success) + { + return BadRequest(res.Errors); + } + return Ok(res.Data); + } + // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} + [HttpGet] + [Route("threads/{id}/likes")] + public async Task GetLikes([FromRoute] int id) + { + var likes = await _threadLikeService.GetLikes(id); + if (!likes.success) + { + return BadRequest(likes.Errors); + } + return Ok(likes.Data); + } + + // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + [HttpDelete] + [Route("threads/{id}/likes")] + public async Task UnlikeThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var res = await _threadLikeService.RemoveLike(id, user.Data.Id); + if (!res.success) + { + return BadRequest(res.Errors); + } + + return Ok(new { message = "Thread unliked successfully" }); + } } \ No newline at end of file diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index bbcc284..0bad463 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -57,6 +57,7 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs b/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs index 16c3cfc..cf68455 100644 --- a/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs +++ b/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs @@ -2,7 +2,7 @@ namespace AskFm.BLL.DTO; public class ThreadAnswerDto { - public int threadId; - public string answer; + public int ThreadId {get;set;} + public string Answer {get;set;} } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs new file mode 100644 index 0000000..1cc52d5 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs @@ -0,0 +1,12 @@ +namespace AskFm.BLL.DTO; + +public class ThreadLikeResponseDto +{ + public int userId {get;set;} + public int threadId {get;set;} + public DateTime createdAt {get;set;} + public string UserName {get;set;} + string ProfilePicture {get;set;} + + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IThreadLikeService.cs b/AskFm/AskFm.BLL/Services/IThreadLikeService.cs new file mode 100644 index 0000000..156e8d8 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IThreadLikeService.cs @@ -0,0 +1,11 @@ +using AskFm.BLL.DTO; +using Microsoft.AspNetCore.Mvc; + +namespace AskFm.BLL.Services; + +public interface IThreadLikeService +{ + public Task> AddLike(int id, int userId); + public Task>> GetLikes(int id); + public Task> RemoveLike(int threadId, int userId); +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs new file mode 100644 index 0000000..622c5aa --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs @@ -0,0 +1,202 @@ +using AskFm.BLL.DTO; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace AskFm.BLL.Services; + +public class ThreadLikeService : IThreadLikeService +{ + private IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public ThreadLikeService(IUnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + // add a like on the Thread that has id = id + public async Task> AddLike(int id, int userId) + { + try + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + // get the thread, Including the ThreadLike Collection + var thread = await _unitOfWork.Threads.FindAsync( + predicate: thread => thread.Id == id, + includes: new[] { "ThreadLikes" } + ); + + // check if thread exists + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // check if user has already liked this thread + var existingLike = thread.ThreadLikes?.FirstOrDefault(like => like.UserId == userId); + if (existingLike != null) + { + // if the The use already liked this thread + if (!existingLike.IsDeleted) + { + var errors = new List() + { + "User has already liked this thread" + }; + await transaction.RollbackAsync(); + return await ServiceResult.Failure(errors); + } + + // otherwise , the user liked the comment , then unliked it , and then wants to like it again + existingLike.IsDeleted = false; + thread.ThreadLikes.Add(existingLike); + _unitOfWork.Threads.Update(thread); + await _unitOfWork.SaveAsync(); + await transaction.CommitAsync(); + + _logger.LogInformation("Like added successfully for thread id: {threadId}", thread.Id); + + + // updating the createdAt column to Now , ignoring the first time the user liked the comment + existingLike.CreatedAt = DateTime.Now; + return await ServiceResult.Success(new ThreadLikeResponseDto() + { + threadId = existingLike.ThreadId, + userId = existingLike.UserId, + createdAt = existingLike.CreatedAt + }); + } + + + try + { + // create a new ThreadLike + var threadLike = new ThreadLike + { + ThreadId = id, + UserId = userId, + CreatedAt = DateTime.Now + }; + + // add the ThreadLike to the Thread + thread.ThreadLikes?.Add(threadLike); + + // update the thread + await _unitOfWork.Threads.UpdateAsync(thread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + // create and return the response DTO + var response = new ThreadLikeResponseDto + { + threadId = threadLike.ThreadId, + userId = threadLike.UserId, + createdAt = threadLike.CreatedAt + }; + + return await ServiceResult.Success(response); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + throw; + } + } + catch (Exception e) + { + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetLikes(int id) + { + try + { + // get the thread, Including the ThreadLikes collection + var thread = await _unitOfWork.Threads.FindAsync( + predicate: thread => thread.Id == id, + includes: new[] { "ThreadLikes.User" } + ); + + // check if thread exists + if (thread == null) + { + return await ServiceResult>.Failure(new List() + { "Thread not found" }); + } + + // return the list of likes + var likes = thread.ThreadLikes.Select(like => new ThreadLikeResponseDto + { + threadId = like.ThreadId, + userId = like.UserId, + UserName = like.User?.Name ?? "Unknown", + createdAt = like.CreatedAt + }).ToList(); + + return await ServiceResult>.Success(likes); + } + catch (Exception e) + { + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + // Remove a like from the thread with id = threadId + public async Task> RemoveLike(int threadId, int userId) + { + try + { + // get the thread with its likes + var thread = await _unitOfWork.Threads.FindAsync( + predicate: thread => thread.Id == threadId, + includes: new[] { "ThreadLikes" } + ); + + // check if thread exists + if (thread == null) + { + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // find the like to remove + var likeToRemove = thread.ThreadLikes?.FirstOrDefault(like => like.UserId == userId); + if (likeToRemove == null) + { + return await ServiceResult.Failure(new List() { "Like not found for this user" }); + } + + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // remove the like from the thread + thread.ThreadLikes?.Remove(likeToRemove); + + // update the thread + await _unitOfWork.Threads.UpdateAsync(thread); + await _unitOfWork.SaveAsync(); + + transaction.Commit(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + throw; + } + } + catch (Exception e) + { + return await ServiceResult.Failure(new List() { e.Message }); + } + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs index 080b0eb..c9b46a1 100644 --- a/AskFm/AskFm.BLL/Services/ThreadService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -84,7 +84,7 @@ public async Task> AddThread(int askerId, Creat await _unitOfWork.Users.UpdateAsync(askerUser); await _unitOfWork.SaveAsync(); - transaction.Commit(); + await transaction.CommitAsync(); var responseDto = new ThreadResponseDto { @@ -150,7 +150,7 @@ public async Task> AnswerThread(ThreadAnswerDto try { // get thread - var threadId = threadAnswerDto.threadId; + var threadId = threadAnswerDto.ThreadId; var thread = await _unitOfWork.Threads.GetByIdAsync(threadId); // chekc if thread if (thread == null) @@ -158,7 +158,7 @@ public async Task> AnswerThread(ThreadAnswerDto return await ServiceResult.Failure(new List() { "Could not find thread" }); } // put the answer on it - thread.AnswerContent = threadAnswerDto.answer; + thread.AnswerContent = threadAnswerDto.Answer; // save changes await _unitOfWork.Threads.UpdateAsync(thread); await _unitOfWork.SaveAsync(); From cb8a0eac29483238f591f91cae0b5192af9b705d Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 6 Oct 2025 15:31:58 +0300 Subject: [PATCH 89/92] Refactor: resolved most of the notes --- AskFm/AskFm.API/AskFm.API.csproj | 2 +- AskFm/AskFm.API/Controllers/ThreadController.cs | 1 - AskFm/AskFm.API/Program.cs | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index 60af690..932256c 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -7,7 +7,7 @@ - + diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs index 3a9a539..be5e545 100644 --- a/AskFm/AskFm.API/Controllers/ThreadController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -83,7 +83,6 @@ void AnswerQuestion([FromRoute] string id) } // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} - // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 0bad463..6d7c379 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -50,6 +50,7 @@ public static void Main(string[] args) .UseSqlServer(ConnectionString)); // ------------------------------------------------- // Register the repositories and services + builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); From 155fd0cd7bea1e09c6e9e19fee943c2142ffe5e9 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Wed, 8 Oct 2025 17:27:54 +0300 Subject: [PATCH 90/92] Finished the Threads , implemented the Saved endpoints and modified the rest --- .../AskFm.API/Controllers/ThreadController.cs | 155 ++++++- AskFm/AskFm.BLL/AskFm.BLL.csproj | 3 +- AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs | 6 + AskFm/AskFm.BLL/DTO/PagedResponseDto.cs | 10 + AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs | 8 - AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs | 2 +- AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs | 21 +- .../AskFm.BLL/Services/CommentLikeService.cs | 3 +- AskFm/AskFm.BLL/Services/IThreadService.cs | 16 +- AskFm/AskFm.BLL/Services/ThreadLikeService.cs | 5 +- AskFm/AskFm.BLL/Services/ThreadService.cs | 421 ++++++++++++++++-- AskFm/AskFm.DAL/Enums/ThreadStatus.cs | 4 +- AskFm/AskFm.DAL/Interfaces/IRepository.cs | 37 +- AskFm/AskFm.DAL/Repositories/Repository.cs | 112 +++-- 14 files changed, 665 insertions(+), 138 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs create mode 100644 AskFm/AskFm.BLL/DTO/PagedResponseDto.cs delete mode 100644 AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs index be5e545..c995dee 100644 --- a/AskFm/AskFm.API/Controllers/ThreadController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -25,12 +25,12 @@ public ThreadController( _userService = userService; _threadService = threadService; } - - + + // POST api/threads/ - Ask a question [HttpPost] [Route("thread")] - public async Task AskQuestion(CreateThreadDto createThreadDto) + public async Task AskQuestion(CreateThreadDto createThreadDto) { // Asker Id -> Current User var user = await _userService.GetCurrentUserAsync(); @@ -39,7 +39,7 @@ public async Task AskQuestion(CreateThreadDto createThreadDto) return BadRequest(user.Errors); } var userId = user.Data.Id; - + var result = await _threadService.AddThread(userId, createThreadDto); if (!result.success) @@ -61,34 +61,153 @@ public async Task GetAllThreads([FromRoute] int id) { return BadRequest(threads.Errors); } - return Ok(threads.Data); + return Ok(threads.Data); } - - // GET api/threads/{id} - Getting the Thread with id = {id} + // GET api/threads/{id} - Getting the Thread with id = {id} [HttpGet] [Route("threads/{id}")] - void GetThreadWithId([FromRoute] string id) + public async Task GetThreadWithId([FromRoute] int id) { - + var thread = await _threadService.GetThreadById(id); + if (!thread.success) + { + return BadRequest(thread.Errors); + } + return Ok(thread.Data); } - + // PUT api/threads/{id}/answer - Add an Answer on the thread with id = {id} [HttpPut] [Route("threads/{id}/answer")] - void AnswerQuestion([FromRoute] string id) + public async Task AnswerQuestion([FromRoute] int id, [FromBody] AnswerThreadDto answerDto) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.AnswerThread(id, user.Data.Id, answerDto); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(result.Data); + } + + // GET api/threads - Get all threads (with pagination) + [HttpGet] + [Route("threads")] + public async Task GetThreads([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var threads = await _threadService.GetThreads(page, pageSize); + if (!threads.success) + { + return BadRequest(threads.Errors); + } + return Ok(threads.Data); + } + + // DELETE api/threads/{id} - Delete a thread + [HttpDelete] + [Route("threads/{id}")] + public async Task DeleteThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.DeleteThread(id, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Thread deleted successfully" }); + } + + // GET api/threads/feed - Get personalized feed for current user + [HttpGet] + [Route("threads/feed")] + public async Task GetFeed([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var feed = await _threadService.GetFeed(user.Data.Id, page, pageSize); + if (!feed.success) + { + return BadRequest(feed.Errors); + } + + return Ok(feed.Data); + } + + // POST api/threads/{id}/save - Save a thread + [HttpPost] + [Route("threads/{id}/save")] + public async Task SaveThread([FromRoute] int id) { - + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.SaveThread(id, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Thread saved successfully" }); } - // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} - // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} - // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + // DELETE api/threads/{id}/save - Unsave a thread + [HttpDelete] + [Route("threads/{id}/save")] + public async Task UnsaveThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.UnsaveThread(id, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Thread unsaved successfully" }); + } + // GET api/threads/saved - Get all saved threads for current user + [HttpGet] + [Route("threads/saved")] + public async Task GetSavedThreads([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } - // POST api/threads/{id}/comments - Add a comment to the thread with id = {id} - // GET api/threads/{id}/comments - Get all the comments to the thread with id = {id} - // DELETE api/threads/{id}/comments - Remove the comment to the thread with id = {id} + var threads = await _threadService.GetSavedThreads(user.Data.Id, page, pageSize); + if (!threads.success) + { + return BadRequest(threads.Errors); + } + return Ok(threads.Data); + } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index bc89689..5bfac97 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -9,9 +9,8 @@ + - - diff --git a/AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs b/AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs new file mode 100644 index 0000000..b6a118e --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs @@ -0,0 +1,6 @@ +namespace AskFm.BLL.DTO; + +public class AnswerThreadDto +{ + public string AnswerContent { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs new file mode 100644 index 0000000..3b693f6 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs @@ -0,0 +1,10 @@ +namespace AskFm.BLL.DTO; + +public class PagedResponseDto +{ + public List Items { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } + public int TotalCount { get; set; } + public int TotalPages { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs b/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs deleted file mode 100644 index cf68455..0000000 --- a/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AskFm.BLL.DTO; - -public class ThreadAnswerDto -{ - public int ThreadId {get;set;} - public string Answer {get;set;} - -} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs index 1cc52d5..f5f82fe 100644 --- a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs @@ -6,7 +6,7 @@ public class ThreadLikeResponseDto public int threadId {get;set;} public DateTime createdAt {get;set;} public string UserName {get;set;} - string ProfilePicture {get;set;} + public string ProfilePicture {get;set;} } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs index 1a6775c..c713574 100644 --- a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs @@ -5,23 +5,16 @@ namespace AskFm.BLL.DTO; public class ThreadResponseDto { public int Id { get; set; } - public string QuestionContent { get; set; } - + public string? AnswerContent { get; set; } public ThreadStatus Status { get; set; } - public bool IsAnonymous { get; set; } - public DateTime CreatedAt { get; set; } - - // Simplified user info instead of full objects - public int AskerId { get; set; } - - public string AskerName { get; set; } - + public int? AskerId { get; set; } + public string? AskerName { get; set; } public int AskedId { get; set; } - - public string AskedName { get; set; } - - public string answer; + public string? AskedName { get; set; } + public int LikesCount { get; set; } + public int CommentsCount { get; set; } + public DateTime? SavedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs index 2033711..a11dd37 100644 --- a/AskFm/AskFm.BLL/Services/CommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -4,7 +4,6 @@ using AskFm.DAL.Models; using AutoMapper; using Microsoft.Extensions.Logging; -using Microsoft.VisualBasic; namespace AskFm.BLL.Services; @@ -111,6 +110,7 @@ public async Task> AddLikeAsync(int commentId, int // otherwise , the user liked the comment , then unliked it , and then wants to like it again existingLike.IsDeleted = false; comment.LikeCount++; + existingLike.CreatedAt = DateTime.Now; _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); await transaction.CommitAsync(); @@ -119,7 +119,6 @@ public async Task> AddLikeAsync(int commentId, int // updating the createdAt column to Now , ignoring the first time the user liked the comment - existingLike.CreatedAt = DateTime.Now; return await ServiceResult.Success(new CommentLikeDto { CommentId = existingLike.CommentId, diff --git a/AskFm/AskFm.BLL/Services/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs index 7f550fc..09fa151 100644 --- a/AskFm/AskFm.BLL/Services/IThreadService.cs +++ b/AskFm/AskFm.BLL/Services/IThreadService.cs @@ -1,10 +1,18 @@ using AskFm.BLL.DTO; -using Thread = AskFm.DAL.Models.Thread; +using Microsoft.AspNetCore.Mvc; namespace AskFm.BLL.Services; + public interface IThreadService { - Task> AddThread(int userId, CreateThreadDto createThreadDto); - Task>> GetAllThreads(int userId); - Task> AnswerThread(ThreadAnswerDto threadAnswerDto); + public Task> AddThread(int askerId, CreateThreadDto createThreadDto); + public Task> GetThreadById(int id); + public Task>> GetAllThreads(int askedId); + public Task> AnswerThread(int threadId, int userId, AnswerThreadDto answerDto); + public Task>> GetThreads(int page, int pageSize); + public Task> DeleteThread(int threadId, int userId); + public Task>> GetFeed(int userId, int page, int pageSize); + public Task> SaveThread(int threadId, int userId); + public Task> UnsaveThread(int threadId, int userId); + public Task>> GetSavedThreads(int userId, int page, int pageSize); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs index 622c5aa..29fcbfe 100644 --- a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs @@ -13,10 +13,11 @@ public class ThreadLikeService : IThreadLikeService private readonly ILogger _logger; private readonly IMapper _mapper; - public ThreadLikeService(IUnitOfWork unitOfWork, ILogger logger) + public ThreadLikeService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper) { _unitOfWork = unitOfWork; _logger = logger; + _mapper = mapper; } // add a like on the Thread that has id = id @@ -55,6 +56,7 @@ public async Task> AddLike(int id, int user // otherwise , the user liked the comment , then unliked it , and then wants to like it again existingLike.IsDeleted = false; + existingLike.CreatedAt = DateTime.Now; thread.ThreadLikes.Add(existingLike); _unitOfWork.Threads.Update(thread); await _unitOfWork.SaveAsync(); @@ -64,7 +66,6 @@ public async Task> AddLike(int id, int user // updating the createdAt column to Now , ignoring the first time the user liked the comment - existingLike.CreatedAt = DateTime.Now; return await ServiceResult.Success(new ThreadLikeResponseDto() { threadId = existingLike.ThreadId, diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs index c9b46a1..6674763 100644 --- a/AskFm/AskFm.BLL/Services/ThreadService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -1,5 +1,7 @@ using AskFm.BLL.DTO; +using AskFm.DAL.Enums; using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; using AutoMapper; using Microsoft.Extensions.Logging; using Thread = AskFm.DAL.Models.Thread; @@ -9,15 +11,16 @@ namespace AskFm.BLL.Services; public class ThreadService : IThreadService { private IUnitOfWork _unitOfWork; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IMapper _mapper; - public ThreadService(IUnitOfWork unitOfWork, ILogger logger) + public ThreadService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper) { _unitOfWork = unitOfWork; _logger = logger; + _mapper = mapper; } - + public async Task> AddThread(int askerId, CreateThreadDto createThreadDto) { var transaction = await _unitOfWork.BeginTransactionAsync(); @@ -103,86 +106,426 @@ public async Task> AddThread(int askerId, Creat } catch (Exception e) { - await transaction.RollbackAsync(); + _logger.LogError(e, "Error adding thread"); return await ServiceResult.Failure(new List() { e.Message }); } } - public async Task>> GetAllThreads(int userId) + public async Task> GetThreadById(int id) { try { - var user = await _unitOfWork.Users.FindAsync( - predicate: u => u.Id == userId, - includes: new[] { "ReceivedThreads" } + var thread = await _unitOfWork.Threads.FindAsync( + predicate: t => t.Id == id, + includes: new[] { "Asker", "Asked", "Comments", "ThreadLikes" } ); + if (thread == null) + { + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + var threadDto = new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 + }; + + return await ServiceResult.Success(threadDto); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetAllThreads(int askedId) + { + try + { + // Get all threads for user + var threads = await _unitOfWork.Threads.FindAllAsync( + predicate: t => t.AskedId == askedId, + includes: new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); - if (user == null) + if (threads == null || !threads.Any()) { - return await ServiceResult>.Failure( - new List() { "Could not find user" }); + return await ServiceResult>.Success(new List()); } - var res = user.ReceivedThreads.Select(t => new ThreadResponseDto - { - Id = t.Id, - QuestionContent = t.QuestionContent, - IsAnonymous = t.isAnonymous, - CreatedAt = t.CreatedAt, - AskedId = t.AskedId, - Status = t.Status, - AskedName = t.Asked?.Name ?? "Unknown", - AskerId = t.AskerId.Value, - AskerName = t.Asker?.Name ?? "Unknown" + var threadDtos = threads.Select(thread => new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 }).ToList(); - return await ServiceResult>.Success(res); + return await ServiceResult>.Success(threadDtos); } catch (Exception e) { + _logger.LogError(e, "Error retrieving threads"); return await ServiceResult>.Failure(new List() { e.Message }); } } - public async Task> AnswerThread(ThreadAnswerDto threadAnswerDto) + public async Task> AnswerThread(int threadId, int userId, + AnswerThreadDto answerDto) { + var transaction = await _unitOfWork.BeginTransactionAsync(); + try { - // get thread - var threadId = threadAnswerDto.ThreadId; - var thread = await _unitOfWork.Threads.GetByIdAsync(threadId); - // chekc if thread + var thread = await _unitOfWork.Threads.FindAsync( + predicate: t => t.Id == threadId, + includes: new[] { "Asker", "Asked" } + ); + if (thread == null) { - return await ServiceResult.Failure(new List() { "Could not find thread" }); + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); } - // put the answer on it - thread.AnswerContent = threadAnswerDto.Answer; - // save changes + + if (thread.AskedId != userId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() + { "User not authorized to answer this thread" }); + } + + thread.AnswerContent = answerDto.AnswerContent; + thread.Status = ThreadStatus.Answered; + await _unitOfWork.Threads.UpdateAsync(thread); await _unitOfWork.SaveAsync(); - ThreadResponseDto threadRes = new ThreadResponseDto() + await transaction.CommitAsync(); + + var threadDto = new ThreadResponseDto { Id = thread.Id, QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, IsAnonymous = thread.isAnonymous, CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, AskedId = thread.AskedId, - AskedName = thread?.Asked.Name ?? "Unknown", - AskerId = thread.AskerId.Value, - AskerName = thread.isAnonymous? "Unknown" : thread?.Asker.Name, - answer = thread.AnswerContent, - Status = thread.Status, + AskedName = thread.Asked?.Name }; - return await ServiceResult.Success(threadRes); + + return await ServiceResult.Success(threadDto); } catch (Exception e) { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error answering thread"); return await ServiceResult.Failure(new List() { e.Message }); } } + public async Task>> GetThreads(int page, int pageSize) + { + try + { + int skipCount = (page - 1) * pageSize; + + var totalCount = await _unitOfWork.Threads.CountAsync(); + + var threads = await _unitOfWork.Threads.GetPagedAsync( + skipCount, + pageSize, + t => t.CreatedAt, + false, + t => true, + new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); + + var threadDtos = threads.Select(thread => new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 + }).ToList(); + + var response = new PagedResponseDto + { + Items = threadDtos, + PageNumber = page, + PageSize = pageSize, + TotalCount = totalCount, + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving threads"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> DeleteThread(int threadId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Get the thread + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // Check if user is authorized to delete this thread (either asker or asked) + if (thread.AskerId != userId && thread.AskedId != userId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() + { "User not authorized to delete this thread" }); + } + + // Delete the thread + await _unitOfWork.Threads.RemoveAsync(thread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error deleting thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetFeed(int userId, int page, int pageSize) + { + try + { + int skipCount = (page - 1) * pageSize; + + var followedUsers = await _unitOfWork.Follows.FindAllAsync(f => f.FollowerId == userId); + var followedUserIds = followedUsers.Select(f => f.FollowedId).ToList(); + + followedUserIds.Add(userId); + + var totalCount = await _unitOfWork.Threads.CountAsync(t => + followedUserIds.Contains(t.AskedId) && t.Status == ThreadStatus.Answered); + + var threads = await _unitOfWork.Threads.GetPagedAsync( + skipCount, + pageSize, + t => t.CreatedAt, + false, + t => followedUserIds.Contains(t.AskedId) && t.Status == ThreadStatus.Answered, + new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); + + var threadDtos = threads.Select(thread => new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 + }).ToList(); + + var response = new PagedResponseDto + { + Items = threadDtos, + PageNumber = page, + PageSize = pageSize, + TotalCount = totalCount, + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving feed"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> SaveThread(int threadId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Check if thread exists + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // Check if thread is already saved + var existingSave = await _unitOfWork.SavedThreads.FindAsync( + st => st.SavedThreadId == threadId && st.UserId == userId + ); + + if (existingSave != null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread already saved" }); + } + + // Create saved thread + var savedThread = new SavedThreads + { + SavedThreadId = threadId, + UserId = userId, + CreatedAt = DateTime.Now + }; + + // Add saved thread + await _unitOfWork.SavedThreads.AddAsync(savedThread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error saving thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task> UnsaveThread(int threadId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Find saved thread + var savedThread = await _unitOfWork.SavedThreads.FindAsync( + st => st.SavedThreadId == threadId && st.UserId == userId + ); + + if (savedThread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not saved" }); + } + + // Remove saved thread + await _unitOfWork.SavedThreads.RemoveAsync(savedThread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error unsaving thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetSavedThreads(int userId, int page, + int pageSize) + { + try + { + int skipCount = (page - 1) * pageSize; + + var totalCount = await _unitOfWork.SavedThreads.CountAsync(st => st.UserId == userId); + + var savedThreads = await _unitOfWork.SavedThreads.GetPagedAsync( + skipCount, + pageSize, + st => st.CreatedAt, + false, + st => st.UserId == userId, + new[] { "Thread", "Thread.Asker", "Thread.Asked", "Thread.Comments", "Thread.ThreadLikes" } + ); + + var threadDtos = savedThreads.Select(st => new ThreadResponseDto + { + Id = st.Thread.Id, + QuestionContent = st.Thread.QuestionContent, + AnswerContent = st.Thread.AnswerContent, + Status = st.Thread.Status, + IsAnonymous = st.Thread.isAnonymous, + CreatedAt = st.Thread.CreatedAt, + AskerId = st.Thread.AskerId ?? 0, + AskerName = st.Thread.isAnonymous ? "Anonymous" : st.Thread.Asker?.Name, + AskedId = st.Thread.AskedId, + AskedName = st.Thread.Asked?.Name, + LikesCount = st.Thread.ThreadLikes?.Count ?? 0, + CommentsCount = st.Thread.Comments?.Count ?? 0, + SavedAt = st.CreatedAt + }).ToList(); + + var response = new PagedResponseDto + { + Items = threadDtos, + PageNumber = page, + PageSize = pageSize, + TotalCount = totalCount, + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving saved threads"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Enums/ThreadStatus.cs b/AskFm/AskFm.DAL/Enums/ThreadStatus.cs index 8bd4d98..7075457 100644 --- a/AskFm/AskFm.DAL/Enums/ThreadStatus.cs +++ b/AskFm/AskFm.DAL/Enums/ThreadStatus.cs @@ -4,5 +4,7 @@ public enum ThreadStatus PRIVATE, PUBLIC, Closed, - PRIVATEQUESTION + PRIVATEQUESTION, + Answered, + Pending } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IRepository.cs b/AskFm/AskFm.DAL/Interfaces/IRepository.cs index 27b6962..469cd08 100644 --- a/AskFm/AskFm.DAL/Interfaces/IRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/IRepository.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; namespace AskFm.DAL.Interfaces; @@ -6,31 +7,39 @@ public interface IRepository where T : class { IQueryable GetAll(); Task> GetAllAsync(); - - + + T? GetById(int id); Task GetByIdAsync(int id); - - + + IQueryable FindAll(Expression> predicate, string[] includes = null); Task> FindAllAsync(Expression> predicate = null, string[] includes = null); - - + + T? Find(Expression> predicate, string[] includes = null); Task FindAsync(Expression> predicate, string[] includes = null); - - + + void Add(T entity); Task AddAsync(T entity); - - + + void Update(T entity); Task UpdateAsync(T entity); - - + + void Remove(T entity); Task RemoveAsync(T entity); - - + + Task CountAsync(Expression>? predicate = null); + + Task> GetPagedAsync( + int skip, + int take, + Expression> orderBy, + bool ascending = true, + Expression>? predicate = null, + string[]? includes = null); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/Repository.cs b/AskFm/AskFm.DAL/Repositories/Repository.cs index e928038..7b4e8fd 100644 --- a/AskFm/AskFm.DAL/Repositories/Repository.cs +++ b/AskFm/AskFm.DAL/Repositories/Repository.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore; namespace AskFm.DAL.Repositories; -public class Repository : IRepository where T : class +public class Repository : IRepository where T : class { protected readonly AppDbContext _context; private readonly DbSet _dbSet; @@ -13,21 +13,21 @@ public Repository(AppDbContext context) _context = context; _dbSet = context.Set(); } - - - + + + public IQueryable GetAll() => _dbSet.AsQueryable(); public async Task> GetAllAsync() => await _dbSet.ToListAsync(); - - - - public T? GetById(int id) => _dbSet.Find(id); + + + + public T? GetById(int id) => _dbSet.Find(id); public async Task GetByIdAsync(int id) => await _dbSet.FindAsync(id); - - - - + + + + public IQueryable FindAll(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; @@ -50,14 +50,14 @@ public async Task> FindAllAsync(Expression> predica return await query.Where(predicate).ToListAsync(); } - - - - + + + + public T? Find(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; - + if (includes != null) { foreach (var include in includes) @@ -65,14 +65,14 @@ public async Task> FindAllAsync(Expression> predica query = query.Include(include); } } - - return query.FirstOrDefault(predicate); + + return query.FirstOrDefault(predicate); } - + public async Task FindAsync(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; - + if (includes != null) { foreach (var include in includes) @@ -80,29 +80,29 @@ public async Task> FindAllAsync(Expression> predica query = query.Include(include); } } - + return await query.FirstOrDefaultAsync(predicate); } - - - - + + + + public void Add(T entity) => _dbSet.Add(entity); public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity); - - - - + + + + public void Update(T entity) => _dbSet.Update(entity); - + public Task UpdateAsync(T entity) { _dbSet.Update(entity); return Task.CompletedTask; } - - + + public void Remove(T entity) => _dbSet.Remove(entity); public Task RemoveAsync(T entity) @@ -111,4 +111,50 @@ public Task RemoveAsync(T entity) return Task.CompletedTask; } + public async Task CountAsync(Expression>? predicate = null) + { + IQueryable query = _dbSet; + + if (predicate != null) + { + query = query.Where(predicate); + } + + return await query.CountAsync(); + } + + public async Task> GetPagedAsync( + int skip, + int take, + Expression> orderBy, + bool ascending = true, + Expression>? predicate = null, + string[]? includes = null) + { + IQueryable query = _dbSet; + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (includes != null) + { + foreach (var include in includes) + { + query = query.Include(include); + } + } + + if (ascending) + { + query = query.OrderBy(orderBy); + } + else + { + query = query.OrderByDescending(orderBy); + } + + return await query.Skip(skip).Take(take).ToListAsync(); + } } \ No newline at end of file From bb19796162ee66715ce485c015914105adbd1342 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sat, 11 Oct 2025 22:34:33 +0300 Subject: [PATCH 91/92] Finish the Comment Controller and service section --- .../Controllers/CommentController.cs | 97 +++++--- .../AskFm.API/Controllers/ThreadController.cs | 4 +- .../Controllers/ThreadLikeController.cs | 4 +- .../Controllers/WeatherForecastController.cs | 2 +- AskFm/AskFm.BLL/DTO/CommentResponseDto.cs | 13 ++ AskFm/AskFm.BLL/DTO/CreateCommentDto.cs | 6 + AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs | 2 +- AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs | 6 +- .../AskFm.BLL/Services/CommentLikeService.cs | 2 +- AskFm/AskFm.BLL/Services/CommentService.cs | 217 +++++++++++++++++- AskFm/AskFm.BLL/Services/ICommentService.cs | 13 +- AskFm/AskFm.BLL/Services/IThreadService.cs | 1 - AskFm/AskFm.BLL/Services/ThreadLikeService.cs | 22 +- AskFm/AskFm.BLL/Services/ThreadService.cs | 4 +- .../UserIdentityService/UserService.cs | 2 +- AskFm/AskFm.DAL/Repositories/Repository.cs | 20 +- 16 files changed, 338 insertions(+), 77 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/CommentResponseDto.cs create mode 100644 AskFm/AskFm.BLL/DTO/CreateCommentDto.cs diff --git a/AskFm/AskFm.API/Controllers/CommentController.cs b/AskFm/AskFm.API/Controllers/CommentController.cs index 21887c9..3af4fa5 100644 --- a/AskFm/AskFm.API/Controllers/CommentController.cs +++ b/AskFm/AskFm.API/Controllers/CommentController.cs @@ -13,13 +13,11 @@ namespace AskFm.API.Controllers; [Authorize(AuthenticationSchemes = "Bearer")] public class CommentController : ControllerBase { - private readonly ICommentLikeService _commentLikeService; private readonly ICommentService _commentService; private readonly ILogger _logger; private readonly IUserService _userService; - - + public CommentController( ICommentLikeService commentLikeService, ICommentService commentService, @@ -31,11 +29,7 @@ public CommentController( _commentService = commentService; _userService = userService; } - - - - - + // GET api/comment/{id}/likes -> get all the likes for a Comment with id = id [HttpGet("{id}/likes")] public async Task GetAllLikes(int id) @@ -55,7 +49,6 @@ public async Task GetAllLikes(int id) return NotFound(new { message = ex.Message, - }); } catch (Exception ex) @@ -65,9 +58,6 @@ public async Task GetAllLikes(int id) } } - - - // POST api/comment/{id}/likes -> add a like for a Comment with id = id [HttpPost("{id}/likes")] public async Task AddLike(int id) @@ -75,21 +65,21 @@ public async Task AddLike(int id) try { var user = await _userService.GetCurrentUserAsync(); - + if (!user.success) { return BadRequest(user.Errors); } - + var createdLike = await _commentLikeService.AddLikeAsync(id, user.Data.Id); - + if (!createdLike.success) { return BadRequest(createdLike.Errors); } return CreatedAtAction( - nameof(GetAllLikes), - new { id = id }, + nameof(GetAllLikes), + new { id = id }, createdLike.Data); } catch (ArgumentException ex) @@ -108,37 +98,31 @@ public async Task AddLike(int id) return StatusCode(500, new { message = "An error occurred while adding like" }); } } - - - - + [HttpDelete("{id}/likes")] public async Task DeleteLike(int id) { int userId = 0; try { - var user = await _userService.GetCurrentUserAsync(); - if (user==null || !user.success) + if (user == null || !user.success) return BadRequest(user.Errors); - - + userId = user.Data.Id; var comment = await _commentService.GetCommentAsync(id); - + if (comment == null || !user.success) return BadRequest(user.Errors); - - + var result = await _commentLikeService.DeleteLikeAsync(id, userId); if (!result.success) { return BadRequest(result.Errors); } - + return NoContent(); } catch (ArgumentException ex) @@ -157,5 +141,58 @@ public async Task DeleteLike(int id) return StatusCode(500, new { message = "An error occurred while deleting like" }); } } - + + // POST api/threads/{id}/comments - Add a comment to the thread with id = {id} + [HttpPost] + [Route("threads/{id}/comments")] + public async Task AddComment([FromRoute] int id, [FromBody] CreateCommentDto createCommentDto) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _commentService.AddComment(id, user.Data.Id, createCommentDto); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(result.Data); + } + + // GET api/threads/{id}/comments - Get all comments for the thread with id = {id} + [HttpGet] + [Route("threads/{id}/comments")] + public async Task GetComments([FromRoute] int id, [FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var result = await _commentService.GetCommentsByThreadId(id, page, pageSize); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(result.Data); + } + + // DELETE api/threads/{threadId}/comments/{commentId} - Delete a comment with id = {commentId} + [HttpDelete] + [Route("threads/{threadId}/comments/{commentId}")] + public async Task DeleteComment([FromRoute] int threadId, [FromRoute] int commentId) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _commentService.DeleteComment(threadId, commentId, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Comment deleted successfully" }); + } } \ No newline at end of file diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs index c995dee..d82796f 100644 --- a/AskFm/AskFm.API/Controllers/ThreadController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -12,12 +12,12 @@ namespace AskFm.API.Controllers; [Authorize(AuthenticationSchemes = "Bearer")] public class ThreadController : ControllerBase { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IUserService _userService; private readonly IThreadService _threadService; public ThreadController( - ILogger logger, + ILogger logger, IUserService userService, IThreadService threadService) { diff --git a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs index 0ba9367..e861388 100644 --- a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs @@ -12,12 +12,12 @@ namespace AskFm.API.Controllers; [Authorize(AuthenticationSchemes = "Bearer")] public class ThreadLikeController : ControllerBase { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IUserService _userService; private readonly IThreadLikeService _threadLikeService; public ThreadLikeController( - ILogger logger, + ILogger logger, IUserService userService, IThreadLikeService threadService) { diff --git a/AskFm/AskFm.API/Controllers/WeatherForecastController.cs b/AskFm/AskFm.API/Controllers/WeatherForecastController.cs index a2f0751..bf50c5f 100644 --- a/AskFm/AskFm.API/Controllers/WeatherForecastController.cs +++ b/AskFm/AskFm.API/Controllers/WeatherForecastController.cs @@ -23,7 +23,7 @@ public IEnumerable Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Date = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) diff --git a/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs new file mode 100644 index 0000000..ea64316 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs @@ -0,0 +1,13 @@ +namespace AskFm.BLL.DTO; + +public class CommentResponseDto +{ + public int Id { get; set; } + public string Content { get; set; } + public int? UserId { get; set; } + public string UserName { get; set; } + public int ThreadId { get; set; } + public DateTime CreatedAt { get; set; } + public int LikesCount { get; set; } + public bool IsLiked { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CreateCommentDto.cs b/AskFm/AskFm.BLL/DTO/CreateCommentDto.cs new file mode 100644 index 0000000..51d8ae3 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CreateCommentDto.cs @@ -0,0 +1,6 @@ +namespace AskFm.BLL.DTO; + +public class CreateCommentDto +{ + public string Content { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs index cf524b2..68a00a1 100644 --- a/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs +++ b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs @@ -5,7 +5,7 @@ public class RefreshTokenDto { public string Token { get; set; } public DateTime ExpireOn { get; set; } - public bool IsExpired => DateTime.Now >= ExpireOn; + public bool IsExpired => DateTime.UtcNow >= ExpireOn; public int ExpireAfter { get; set; } public DateTime CreatedOn { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs index f5f82fe..dc0be27 100644 --- a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs @@ -2,9 +2,9 @@ namespace AskFm.BLL.DTO; public class ThreadLikeResponseDto { - public int userId {get;set;} - public int threadId {get;set;} - public DateTime createdAt {get;set;} + public int UserId {get;set;} + public int ThreadId {get;set;} + public DateTime CreatedAt {get;set;} public string UserName {get;set;} public string ProfilePicture {get;set;} diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs index a11dd37..27d0a09 100644 --- a/AskFm/AskFm.BLL/Services/CommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -110,7 +110,7 @@ public async Task> AddLikeAsync(int commentId, int // otherwise , the user liked the comment , then unliked it , and then wants to like it again existingLike.IsDeleted = false; comment.LikeCount++; - existingLike.CreatedAt = DateTime.Now; + existingLike.CreatedAt = DateTime.UtcNow; _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); await transaction.CommitAsync(); diff --git a/AskFm/AskFm.BLL/Services/CommentService.cs b/AskFm/AskFm.BLL/Services/CommentService.cs index 81f7c98..8f55704 100644 --- a/AskFm/AskFm.BLL/Services/CommentService.cs +++ b/AskFm/AskFm.BLL/Services/CommentService.cs @@ -1,28 +1,229 @@ +using AskFm.BLL.DTO; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace AskFm.BLL.Services; public class CommentService : ICommentService { - private IUnitOfWork _unitOfWork; - public CommentService(IUnitOfWork unitOfWork) + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public CommentService(IUnitOfWork unitOfWork, ILogger logger) { _unitOfWork = unitOfWork; + _logger = logger; } - - public async Task> GetCommentAsync(int commentId) + + public async Task> GetCommentAsync(int id) { try { - var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); - return await ServiceResult.Success(comment); + var comment = await _unitOfWork.Comments.FindAsync( + c => c.Id == id, + new[] { "User", "CommentLikes" } + ); + + if (comment == null) + { + return await ServiceResult.Failure(new List() { "Comment not found" }); + } + + var commentDto = new CommentResponseDto + { + Id = comment.Id, + Content = comment.Content, + UserId = comment.UserId, + UserName = comment.User?.Name ?? "Unknown", + ThreadId = comment.ThreadId, + CreatedAt = comment.CreatedAt, + LikesCount = comment.CommentLikes?.Count ?? 0, + }; + + return await ServiceResult.Success(commentDto); } catch (Exception e) { - return await ServiceResult.Failure(new List(){e.Message}); + _logger.LogError(e, "Error retrieving comment with ID {Id}", id); + return await ServiceResult.Failure(new List() { e.Message }); } } - + public async Task> AddComment(int threadId, int userId, CreateCommentDto commentDto) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Check if thread exists + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // Check if user exists + var user = await _unitOfWork.Users.FindAsync(u => u.Id == userId); + if (user == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "User not found" }); + } + + // Validate comment content + if (string.IsNullOrWhiteSpace(commentDto.Content)) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Comment content cannot be empty" }); + } + + // Create comment + var comment = new Comment + { + Content = commentDto.Content, + UserId = userId, + ThreadId = threadId, + CreatedAt = DateTime.UtcNow, + CommentLikes = new List() + }; + + // Add comment to database + await _unitOfWork.Comments.AddAsync(comment); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + // Return response dto + var commentResponseDto = new CommentResponseDto + { + Id = comment.Id, + Content = comment.Content, + UserId = comment.UserId, + UserName = user.Name, + ThreadId = comment.ThreadId, + CreatedAt = comment.CreatedAt, + LikesCount = 0, + }; + + return await ServiceResult.Success(commentResponseDto); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error adding comment to thread {ThreadId}", threadId); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetCommentsByThreadId(int threadId, int page, int pageSize) + { + try + { + // Check if thread exists + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + return await ServiceResult>.Failure(new List() { "Thread not found" }); + } + + // Get total count of comments for this thread + var totalCount = await _unitOfWork.Comments.CountAsync(c => c.ThreadId == threadId); + + // Get paginated comments + var comments = await _unitOfWork.Comments.GetPagedAsync( + skip: (page - 1) * pageSize, + take: pageSize, + orderBy: c => c.CreatedAt, + ascending: false, // Newest first + predicate: c => c.ThreadId == threadId, + includes: new[] { "User", "CommentLikes" } + ); + + var commentDtos = comments.Select(c => new CommentResponseDto + { + Id = c.Id, + Content = c.Content, + UserId = c.UserId, + UserName = c.User?.Name ?? "Unknown", + ThreadId = c.ThreadId, + CreatedAt = c.CreatedAt, + LikesCount = c.CommentLikes?.Count ?? 0, + }).ToList(); + + // Create paged response + var response = new PagedResponseDto + { + Items = commentDtos, + PageNumber = page, + PageSize = pageSize, + TotalCount = totalCount, + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving comments for thread {ThreadId}", threadId); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> DeleteComment(int threadId, int commentId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Check if comment exists + var comment = await _unitOfWork.Comments.FindAsync( + c => c.Id == commentId && c.ThreadId == threadId, + new[] { "CommentLikes" } + ); + + if (comment == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Comment not found" }); + } + + // Check if user is authorized to delete (either comment owner or thread owner) + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + + if (comment.UserId != userId && thread.AskedId != userId && thread.AskerId != userId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "User not authorized to delete this comment" }); + } + + // Remove comment likes first + if (comment.CommentLikes != null && comment.CommentLikes.Any()) + { + foreach (var like in comment.CommentLikes.ToList()) + { + await _unitOfWork.CommentLikes.RemoveAsync(like); + } + } + + // Remove comment + await _unitOfWork.Comments.RemoveAsync(comment); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error deleting comment {CommentId} from thread {ThreadId}", commentId, threadId); + return await ServiceResult.Failure(new List() { e.Message }); + } + } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ICommentService.cs b/AskFm/AskFm.BLL/Services/ICommentService.cs index dda8314..8c58275 100644 --- a/AskFm/AskFm.BLL/Services/ICommentService.cs +++ b/AskFm/AskFm.BLL/Services/ICommentService.cs @@ -1,8 +1,19 @@ +using AskFm.BLL.DTO; using AskFm.DAL.Models; namespace AskFm.BLL.Services; public interface ICommentService { - Task> GetCommentAsync(int commentId); + // Get a specific comment by ID + Task> GetCommentAsync(int id); + + // Add a comment to a thread + Task> AddComment(int threadId, int userId, CreateCommentDto commentDto); + + // Get all comments for a thread with pagination + Task>> GetCommentsByThreadId(int threadId, int page, int pageSize); + + // Delete a specific comment + Task> DeleteComment(int threadId, int commentId, int userId); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs index 09fa151..d329412 100644 --- a/AskFm/AskFm.BLL/Services/IThreadService.cs +++ b/AskFm/AskFm.BLL/Services/IThreadService.cs @@ -1,5 +1,4 @@ using AskFm.BLL.DTO; -using Microsoft.AspNetCore.Mvc; namespace AskFm.BLL.Services; diff --git a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs index 29fcbfe..27facb4 100644 --- a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs @@ -56,7 +56,7 @@ public async Task> AddLike(int id, int user // otherwise , the user liked the comment , then unliked it , and then wants to like it again existingLike.IsDeleted = false; - existingLike.CreatedAt = DateTime.Now; + existingLike.CreatedAt = DateTime.UtcNow; thread.ThreadLikes.Add(existingLike); _unitOfWork.Threads.Update(thread); await _unitOfWork.SaveAsync(); @@ -68,9 +68,9 @@ public async Task> AddLike(int id, int user // updating the createdAt column to Now , ignoring the first time the user liked the comment return await ServiceResult.Success(new ThreadLikeResponseDto() { - threadId = existingLike.ThreadId, - userId = existingLike.UserId, - createdAt = existingLike.CreatedAt + ThreadId = existingLike.ThreadId, + UserId = existingLike.UserId, + CreatedAt = existingLike.CreatedAt }); } @@ -82,7 +82,7 @@ public async Task> AddLike(int id, int user { ThreadId = id, UserId = userId, - CreatedAt = DateTime.Now + CreatedAt = DateTime.UtcNow }; // add the ThreadLike to the Thread @@ -97,9 +97,9 @@ public async Task> AddLike(int id, int user // create and return the response DTO var response = new ThreadLikeResponseDto { - threadId = threadLike.ThreadId, - userId = threadLike.UserId, - createdAt = threadLike.CreatedAt + ThreadId = threadLike.ThreadId, + UserId = threadLike.UserId, + CreatedAt = threadLike.CreatedAt }; return await ServiceResult.Success(response); @@ -136,10 +136,10 @@ public async Task>> GetLikes(int id) // return the list of likes var likes = thread.ThreadLikes.Select(like => new ThreadLikeResponseDto { - threadId = like.ThreadId, - userId = like.UserId, + ThreadId = like.ThreadId, + UserId = like.UserId, UserName = like.User?.Name ?? "Unknown", - createdAt = like.CreatedAt + CreatedAt = like.CreatedAt }).ToList(); return await ServiceResult>.Success(likes); diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs index 6674763..4552a5a 100644 --- a/AskFm/AskFm.BLL/Services/ThreadService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -77,7 +77,7 @@ public async Task> AddThread(int askerId, Creat AnswerContent = "", Status = createThreadDto.Status, isAnonymous = createThreadDto.isAnonymous, - CreatedAt = DateTime.Now, + CreatedAt = DateTime.UtcNow, }; await _unitOfWork.Threads.AddAsync(thread); @@ -424,7 +424,7 @@ public async Task> SaveThread(int threadId, int userId) { SavedThreadId = threadId, UserId = userId, - CreatedAt = DateTime.Now + CreatedAt = DateTime.UtcNow }; // Add saved thread diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 7dcc1a9..fdbf633 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -170,7 +170,7 @@ public async Task> UpdateLastSeenAsync(int userId) await using var transaction = await _unitOfWork.BeginTransactionAsync(); try { - appUser.LastSeen = DateTime.Now; + appUser.LastSeen = DateTime.UtcNow; await _unitOfWork.Users.UpdateAsync(appUser); await _unitOfWork.SaveAsync(); await transaction.CommitAsync(); diff --git a/AskFm/AskFm.DAL/Repositories/Repository.cs b/AskFm/AskFm.DAL/Repositories/Repository.cs index 7b4e8fd..1a3a7c1 100644 --- a/AskFm/AskFm.DAL/Repositories/Repository.cs +++ b/AskFm/AskFm.DAL/Repositories/Repository.cs @@ -1,6 +1,7 @@ using System.Linq.Expressions; using AskFm.DAL.Interfaces; using Microsoft.EntityFrameworkCore; + namespace AskFm.DAL.Repositories; public class Repository : IRepository where T : class @@ -15,19 +16,14 @@ public Repository(AppDbContext context) } - public IQueryable GetAll() => _dbSet.AsQueryable(); public async Task> GetAllAsync() => await _dbSet.ToListAsync(); - - public T? GetById(int id) => _dbSet.Find(id); public async Task GetByIdAsync(int id) => await _dbSet.FindAsync(id); - - public IQueryable FindAll(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; @@ -36,6 +32,7 @@ public IQueryable FindAll(Expression> predicate, string[] inclu foreach (var include in includes) query = query.Include(include); } + return query.Where(predicate); } @@ -51,9 +48,6 @@ public async Task> FindAllAsync(Expression> predica } - - - public T? Find(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; @@ -85,15 +79,11 @@ public async Task> FindAllAsync(Expression> predica } - - public void Add(T entity) => _dbSet.Add(entity); public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity); - - public void Update(T entity) => _dbSet.Update(entity); public Task UpdateAsync(T entity) @@ -104,7 +94,7 @@ public Task UpdateAsync(T entity) public void Remove(T entity) => _dbSet.Remove(entity); - + public Task RemoveAsync(T entity) { _dbSet.Remove(entity); @@ -131,6 +121,10 @@ public async Task> GetPagedAsync( Expression>? predicate = null, string[]? includes = null) { + if (skip < 0) + throw new ArgumentOutOfRangeException(nameof(skip), "Skip must be non-negative"); + if (take <= 0) + throw new ArgumentOutOfRangeException(nameof(take), "Take must be positive"); IQueryable query = _dbSet; if (predicate != null) From 767337672222a7944a0790afcca77e34e0c8bbde Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sat, 29 Nov 2025 20:28:46 +0200 Subject: [PATCH 92/92] refactor(totalCount & totalPages): - removed the totalCount and totalPages from pagedResponseDto as thier uses is so overhead on the database. - instead, replaced them with HasMore, meaning are there any remaining pages left here to make the client or the frontend use as a flage. --- AskFm/AskFm.BLL/DTO/CommentResponseDto.cs | 1 - AskFm/AskFm.BLL/DTO/CreateThreadDto.cs | 6 +- AskFm/AskFm.BLL/DTO/PagedResponseDto.cs | 3 +- AskFm/AskFm.BLL/Services/CommentService.cs | 18 ++--- AskFm/AskFm.BLL/Services/ThreadService.cs | 77 ++++++++++++++++------ 5 files changed, 74 insertions(+), 31 deletions(-) diff --git a/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs index ea64316..f19df0a 100644 --- a/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs @@ -9,5 +9,4 @@ public class CommentResponseDto public int ThreadId { get; set; } public DateTime CreatedAt { get; set; } public int LikesCount { get; set; } - public bool IsLiked { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs index 4b078ab..998c944 100644 --- a/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs +++ b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using AskFm.DAL.Enums; using AskFm.DAL.Models; @@ -5,12 +6,15 @@ namespace AskFm.BLL.DTO; public class CreateThreadDto { + [Required(ErrorMessage = "Asked user ID is required")] public int AskedId { get; set; } + [Required(ErrorMessage = "Question content is required")] + [StringLength(1000, MinimumLength = 2, ErrorMessage = "Question must be between 2 and 1000 characters")] public string QuestionContent { get; set; } public ThreadStatus Status { get; set; } - public bool isAnonymous { get; set; } + public bool IsAnonymous { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs index 3b693f6..e6f0636 100644 --- a/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs @@ -5,6 +5,5 @@ public class PagedResponseDto public List Items { get; set; } public int PageNumber { get; set; } public int PageSize { get; set; } - public int TotalCount { get; set; } - public int TotalPages { get; set; } + public bool HasMore { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/CommentService.cs b/AskFm/AskFm.BLL/Services/CommentService.cs index 8f55704..6400a88 100644 --- a/AskFm/AskFm.BLL/Services/CommentService.cs +++ b/AskFm/AskFm.BLL/Services/CommentService.cs @@ -132,20 +132,23 @@ public async Task>> GetCommen return await ServiceResult>.Failure(new List() { "Thread not found" }); } - // Get total count of comments for this thread - var totalCount = await _unitOfWork.Comments.CountAsync(c => c.ThreadId == threadId); - + + int skip = (page - 1) * pageSize; // Get paginated comments var comments = await _unitOfWork.Comments.GetPagedAsync( - skip: (page - 1) * pageSize, - take: pageSize, + skip: skip, + take: pageSize + 1, orderBy: c => c.CreatedAt, ascending: false, // Newest first predicate: c => c.ThreadId == threadId, includes: new[] { "User", "CommentLikes" } ); + + bool hasMore = comments.Count > pageSize; + + var trimmed = comments.Take(pageSize).ToList(); - var commentDtos = comments.Select(c => new CommentResponseDto + var commentDtos = trimmed.Select(c => new CommentResponseDto { Id = c.Id, Content = c.Content, @@ -162,8 +165,7 @@ public async Task>> GetCommen Items = commentDtos, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, - TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + HasMore = hasMore }; return await ServiceResult>.Success(response); diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs index 4552a5a..77cb2a7 100644 --- a/AskFm/AskFm.BLL/Services/ThreadService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -76,7 +76,7 @@ public async Task> AddThread(int askerId, Creat AnswerContent = "", Status = createThreadDto.Status, - isAnonymous = createThreadDto.isAnonymous, + isAnonymous = createThreadDto.IsAnonymous, CreatedAt = DateTime.UtcNow, }; @@ -97,9 +97,9 @@ public async Task> AddThread(int askerId, Creat IsAnonymous = thread.isAnonymous, CreatedAt = thread.CreatedAt, AskerId = thread.AskerId.Value, - AskerName = thread.Asker?.Name ?? "Unknown", + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, AskedId = thread.AskedId, - AskedName = thread.Asked?.Name ?? "Unknown" + AskedName = askedUser.Name }; return await ServiceResult.Success(responseDto); @@ -116,7 +116,7 @@ public async Task> GetThreadById(int id) try { var thread = await _unitOfWork.Threads.FindAsync( - predicate: t => t.Id == id, + predicate: t => t.Id == id && !t.IsDeleted, includes: new[] { "Asker", "Asked", "Comments", "ThreadLikes" } ); @@ -197,6 +197,21 @@ public async Task> AnswerThread(int threadId, i try { + if (string.IsNullOrWhiteSpace(answerDto.AnswerContent)) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure( + new List { "Answer cannot be empty or contain only whitespace" }); + } + + var trimmedAnswer = answerDto.AnswerContent.Trim(); + if (trimmedAnswer.Length > 5000) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure( + new List { "Answer cannot exceed 5000 characters" }); + } + var thread = await _unitOfWork.Threads.FindAsync( predicate: t => t.Id == threadId, includes: new[] { "Asker", "Asked" } @@ -234,6 +249,8 @@ public async Task> AnswerThread(int threadId, i AskerId = thread.AskerId ?? 0, AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, AskedId = thread.AskedId, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0, AskedName = thread.Asked?.Name }; @@ -253,18 +270,22 @@ public async Task>> GetThreads { int skipCount = (page - 1) * pageSize; - var totalCount = await _unitOfWork.Threads.CountAsync(); + // var totalCount = await _unitOfWork.Threads.CountAsync(); var threads = await _unitOfWork.Threads.GetPagedAsync( skipCount, - pageSize, + pageSize + 1, t => t.CreatedAt, false, t => true, new[] { "Asker", "Asked", "Comments", "ThreadLikes" } ); + + bool hasMore = threads.Count > pageSize; - var threadDtos = threads.Select(thread => new ThreadResponseDto + var trimmed = threads.Take(pageSize).ToList(); + + var threadDtos = trimmed.Select(thread => new ThreadResponseDto { Id = thread.Id, QuestionContent = thread.QuestionContent, @@ -285,8 +306,8 @@ public async Task>> GetThreads Items = threadDtos, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, - TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + HasMore = hasMore + }; return await ServiceResult>.Success(response); @@ -353,14 +374,24 @@ public async Task>> GetFeed(in var threads = await _unitOfWork.Threads.GetPagedAsync( skipCount, - pageSize, + pageSize + 1, t => t.CreatedAt, false, t => followedUserIds.Contains(t.AskedId) && t.Status == ThreadStatus.Answered, new[] { "Asker", "Asked", "Comments", "ThreadLikes" } ); - - var threadDtos = threads.Select(thread => new ThreadResponseDto + + // asking if still there is some remaining pages withou using COUNT() + bool hasMore = threads.Count > pageSize; + + /* after getting the threads with size of pageSize + 1 + to find if there are remaining threads (nextPage) without usint TotalCound and totalPages + now we need to return back only the PageSize of threads + that what i'm doing here in the trimmed list, + */ + var trimmed = threads.Take(pageSize).ToList(); + + var threadDtos = trimmed.Select(thread => new ThreadResponseDto { Id = thread.Id, QuestionContent = thread.QuestionContent, @@ -381,8 +412,7 @@ public async Task>> GetFeed(in Items = threadDtos, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, - TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + HasMore = hasMore }; return await ServiceResult>.Success(response); @@ -483,18 +513,28 @@ public async Task>> GetSavedTh { int skipCount = (page - 1) * pageSize; - var totalCount = await _unitOfWork.SavedThreads.CountAsync(st => st.UserId == userId); + // var totalCount = await _unitOfWork.SavedThreads.CountAsync(st => st.UserId == userId); var savedThreads = await _unitOfWork.SavedThreads.GetPagedAsync( skipCount, - pageSize, + pageSize + 1, st => st.CreatedAt, false, st => st.UserId == userId, new[] { "Thread", "Thread.Asker", "Thread.Asked", "Thread.Comments", "Thread.ThreadLikes" } ); - var threadDtos = savedThreads.Select(st => new ThreadResponseDto + + bool hasMore = savedThreads.Count > pageSize; + + /* after getting the threads with size of pageSize + 1 + to find if there are remaining threads (nextPage) without usint TotalCound and totalPages + now we need to return back only the PageSize of threads + that what i'm doing here in the trimmed list, + */ + var trimmed = savedThreads.Take(pageSize).ToList(); + + var threadDtos = trimmed.Select(st => new ThreadResponseDto { Id = st.Thread.Id, QuestionContent = st.Thread.QuestionContent, @@ -516,8 +556,7 @@ public async Task>> GetSavedTh Items = threadDtos, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, - TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + HasMore = hasMore }; return await ServiceResult>.Success(response);