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 diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index d791ad5..932256c 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -7,15 +7,24 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + + + + @@ -23,4 +32,28 @@ + + net9.0 + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/AskFm/AskFm.API/Controllers/AuthController.cs b/AskFm/AskFm.API/Controllers/AuthController.cs new file mode 100644 index 0000000..501bd3b --- /dev/null +++ b/AskFm/AskFm.API/Controllers/AuthController.cs @@ -0,0 +1,150 @@ +using System.ComponentModel.DataAnnotations; +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; + private IUserService _userService; + + public AuthController(IAuthService authService, IUserService userService) + { + _authService = authService; + _userService = userService; + } + + [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.Errors); + } + 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.Errors); + + } + + setRefreshToken(result.Data.RefreshToken.Token,result.Data.RefreshToken.ExpireOn); + + return Ok(result); + } + + [HttpPost] + [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.Errors); + } + setRefreshToken(result.Data.RefreshToken.Token,result.Data.RefreshToken.ExpireOn); + return Ok(result); + } + + [HttpPost("logout/{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"); + } + + var result = await _authService.Logout(currentUser.Data.Id,refreshToken); + + if (!result.success) + { + return BadRequest(result.Errors); + } + + 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() + { + HttpOnly = true, + Expires = expires.ToLocalTime() + }; + Response.Cookies.Append("refreshToken", refreshToken, cookieOption); + } + +} \ No newline at end of file diff --git a/AskFm/AskFm.API/Controllers/CommentController.cs b/AskFm/AskFm.API/Controllers/CommentController.cs new file mode 100644 index 0000000..3af4fa5 --- /dev/null +++ b/AskFm/AskFm.API/Controllers/CommentController.cs @@ -0,0 +1,198 @@ +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("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; + } + + // 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); + 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, + }); + } + 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) + { + 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 }, + createdLike.Data); + } + 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) + { + int userId = 0; + try + { + var user = await _userService.GetCurrentUserAsync(); + + 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) + { + _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} by user {UserId}", id, userId); + return Forbid(); + } + catch (Exception ex) + { + _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" }); + } + } + + // 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/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs new file mode 100644 index 0000000..a7855da --- /dev/null +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -0,0 +1,120 @@ +using System.Security.Claims; +using AskFm.BLL.DTO; +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; + + public NotificationController(INotificationService notificationService) + { + _notificationService = notificationService; + } + + [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); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [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); + } + 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 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("read-all")] + public async Task MarkAllNotificationsAsRead() + { + try + { + var userId = GetCurrentUserId(); + var result = await _notificationService.MarkAllNotificationsAsRead(userId); + return Ok(new { message = result }); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task CreateNotification([FromBody] CreateNotificationRequest request) + { + try + { + 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() + { + // 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"); + } + return userId; + } + } +} \ No newline at end of file diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs new file mode 100644 index 0000000..d82796f --- /dev/null +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -0,0 +1,213 @@ +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); + + } + + + // get all threads for user by user id + [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("threads/{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")] + 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" }); + } + + // 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); + } + + 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.API/Controllers/ThreadLikeController.cs b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs new file mode 100644 index 0000000..e861388 --- /dev/null +++ b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs @@ -0,0 +1,82 @@ +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 ThreadLikeController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IThreadLikeService _threadLikeService; + + public ThreadLikeController( + ILogger logger, + IUserService userService, + IThreadLikeService threadService) + { + _logger = logger; + _userService = userService; + _threadLikeService = threadService; + } + + + // POST api/threads/{id}/likes - Add a Like 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/Controllers/UserController.cs b/AskFm/AskFm.API/Controllers/UserController.cs new file mode 100644 index 0000000..3f60899 --- /dev/null +++ b/AskFm/AskFm.API/Controllers/UserController.cs @@ -0,0 +1,173 @@ +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("api/[controller]")] +[Authorize(AuthenticationSchemes = "Bearer")] +public class UserController : ControllerBase +{ + 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("profile")] + public async Task GetCurrentUserAsync() + { + var result = await _userService.GetCurrentUserAsync(); + + if (!result.success) + { + return BadRequest(result.Errors); + } + 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); + } + + [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 StatusCode(StatusCodes.Status403Forbidden, "Cannot Update this user"); + } + var result = await _userService.UpdateUserAsync(userId, updatedUser); + if (!result.success) + { + return BadRequest(result.Errors); + } + + var userRead = await _userService.GetUserByIdAsync(userId); + return Ok(userRead); + } + + [HttpDelete] + [Route("profile/{userId}")] + public async Task DeleteUserAsync(int userId) + { + if (!await _checkCurrentUser(userId)) + { + return StatusCode(StatusCodes.Status403Forbidden, "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 StatusCode(StatusCodes.Status403Forbidden, "Cannot Follow this user"); + } + if (!await _checkCurrentUser(followerId)) + { + return StatusCode(StatusCodes.Status403Forbidden, "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 StatusCode(StatusCodes.Status403Forbidden, "Cannot Unfollow the current user"); + } + if (!await _checkCurrentUser(followerId)) + { + return StatusCode(StatusCodes.Status403Forbidden, "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, UpdatePasswordDTO udpatePasswordDto) + { + if (await _checkCurrentUser(userId)) + { + + return StatusCode(StatusCodes.Status403Forbidden, "Invalid Operation"); + } + var result = await _userService.UpdatePassword(userId, udpatePasswordDto); + + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(); + } + + + //------------------------------------------------------------------- + // Helper functions + private async Task _checkCurrentUser(int userId) + { + var current_user = await _userService.GetCurrentUserAsync(); + return current_user.Data.Id == userId; + } + + + /* TODO + update email + confirm email + check user not deleted in login + + */ + +} \ No newline at end of file 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.API/Program.cs b/AskFm/AskFm.API/Program.cs index 807ebe5..6d7c379 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -1,8 +1,28 @@ +using System.Text; using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; using AskFm.DAL; +using AskFm.DAL.Models; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Repositories; using DotNetEnv; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore.Proxies; +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; +using Swashbuckle.AspNetCore.SwaggerGen; +using Castle.Components.DictionaryAdapter.Xml; +using Microsoft.AspNetCore.Identity.UI.Services; +using Shared; +using IEmailSender = AskFm.BLL.Services.IEmailSender; + namespace AskFm.API; + public class Program { public static void Main(string[] args) @@ -12,7 +32,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(); @@ -21,22 +40,237 @@ 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.UseSqlServer(ConnectionString)); + options + .UseLazyLoadingProxies() + .UseSqlServer(ConnectionString)); + // ------------------------------------------------- + // Register the repositories and services + builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddControllers(); + + // Configure Swagger with JWT Authentication + builder.Services.AddSwaggerGen(setup => + { + 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() } + }); + + }); + + + // Authentication & Authorization + + JwtOptions jwtOptions = new JwtOptions + { + Issuer = Environment.GetEnvironmentVariable("ISSUER"), + Audience = Environment.GetEnvironmentVariable("AUDIENCE"), + SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"), + AccessExpiration = builder.Configuration.GetValue("ExpireTimes:Jwt_Token_Exp"), + AccessRefreshTokenExpiration =builder.Configuration.GetValue("ExpireTimes:Refresh_Token_Exp") + }; + if (jwtOptions == null) + { + 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"); + Options.Audience = Environment.GetEnvironmentVariable("AUDIENCE"); + Options.SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"); + 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"; + 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)), + 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"); + } + } + }; + + + // 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(); + + + // Identity + 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(); + + // Redis Cache + builder.Services.AddStackExchangeRedisCache(options => + { + options.Configuration = builder.Configuration.GetConnectionString("Redis"); + options.InstanceName = "AskFmCache"; + }); + + builder.Services.AddSingleton(); + var app = builder.Build(); // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) app.MapOpenApi(); + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.MapOpenApi(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/openapi/v1.json", "api"); + }); + } app.UseHttpsRedirection(); - app.UseAuthorization(); + // Apply CORS before authentication + app.UseCors("SignalRPolicy"); + app.UseAuthentication(); + app.UseAuthorization(); app.MapControllers(); + // Map SignalR Hub + app.MapHub("/notificationHub"); + app.Run(); } } \ No newline at end of file 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" diff --git a/AskFm/AskFm.API/appsettings.json b/AskFm/AskFm.API/appsettings.json index 779f8b0..7749d53 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": { @@ -8,5 +9,16 @@ "Microsoft.AspNetCore": "Warning" } }, + "ExpireTimes": { + "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 ba85168..5bfac97 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -1,14 +1,19 @@  - - - net9.0 - enable - enable - - - - - + + net9.0 + enable + enable + + + + + + + + + + + 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/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/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/CommentResponseDto.cs b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs new file mode 100644 index 0000000..f19df0a --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs @@ -0,0 +1,12 @@ +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; } +} \ 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/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/DTO/CreateThreadDto.cs b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs new file mode 100644 index 0000000..998c944 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using AskFm.DAL.Enums; +using AskFm.DAL.Models; + +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; } + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/EmailSettings.cs b/AskFm/AskFm.BLL/DTO/EmailSettings.cs new file mode 100644 index 0000000..a7953f3 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/EmailSettings.cs @@ -0,0 +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 diff --git a/AskFm/AskFm.BLL/DTO/NotificationDto.cs b/AskFm/AskFm.BLL/DTO/NotificationDto.cs new file mode 100644 index 0000000..91c1fc3 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/NotificationDto.cs @@ -0,0 +1,19 @@ +using AskFm.BLL.DTO; + +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; } + public ActorDto? Actor { get; set; } + public PaginationDto Pagination { get; set; } + + +} diff --git a/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs new file mode 100644 index 0000000..e6f0636 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs @@ -0,0 +1,9 @@ +namespace AskFm.BLL.DTO; + +public class PagedResponseDto +{ + public List Items { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } + public bool HasMore { get; set; } +} \ No newline at end of file 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.BLL/DTO/RefreshTokenDto.cs b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs new file mode 100644 index 0000000..68a00a1 --- /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.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 new file mode 100644 index 0000000..dc0be27 --- /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;} + 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 new file mode 100644 index 0000000..c713574 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs @@ -0,0 +1,20 @@ +using AskFm.DAL.Enums; + +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; } + public int? AskerId { get; set; } + public string? AskerName { get; set; } + public int AskedId { get; set; } + 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/DTO/UserDTOs/AuthResponseDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs new file mode 100644 index 0000000..aa3ebba --- /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 RefreshTokenDto RefreshToken { get; set; } + public ReadUserDTO User { 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/LoginDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs new file mode 100644 index 0000000..6b8b600 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/LoginDTO.cs @@ -0,0 +1,7 @@ +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/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..5433972 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/RegisterUserDTO.cs @@ -0,0 +1,12 @@ +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; } + public string Passwrod { get; set; } + public DateTime LastSeen { 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/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/DTO/UserDTOs/UpdateUserDTO.cs b/AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs new file mode 100644 index 0000000..6e44895 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/UserDTOs/UpdateUserDTO.cs @@ -0,0 +1,9 @@ +namespace AskFm.BLL.DTO.UserDTOs; + +public class UpdateUserDTO +{ + public string Name { get; set; } + public string Bio { get; set; } + public string AvatarPath { get; set; } + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Hub/NotificationHub.cs b/AskFm/AskFm.BLL/Hub/NotificationHub.cs new file mode 100644 index 0000000..500526f --- /dev/null +++ b/AskFm/AskFm.BLL/Hub/NotificationHub.cs @@ -0,0 +1,40 @@ +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) + { + await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}"); + } + + public async Task LeaveUserGroup(string userId) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user_{userId}"); + } + + 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); + } + } +} \ 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..27d0a09 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -0,0 +1,223 @@ +using System.Runtime.InteropServices.JavaScript; +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) + { + var errors = new List() + { + $"Comment with id {commentId} not found" + }; + return await ServiceResult>.Failure(errors); + } + + 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 await ServiceResult>.Success(likeDtos); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving likes for comment id: {CommentId}", commentId); + return await ServiceResult>.Failure(new List() { ex.Message }); + } + } + + 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}", + 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" + }; + await transaction.RollbackAsync(); + return await ServiceResult.Failure(errors); + } + + string userName = user.UserName; + if (comment == null) + { + var errors = new List() + { + $"Comment with id {commentId} not found" + }; + await transaction.RollbackAsync(); + return await ServiceResult.Failure(errors); + } + + var existingLike = await _unitOfWork.CommentLikes.FindAsync( + cl => cl.CommentId == commentId && cl.UserId == userId + ); + + // if the CommentLike already exit in the DB + if (existingLike != null) + { + // if the The use already liked this comment + if (!existingLike.IsDeleted) + { + var errors = new List() + { + "User has already liked this comment" + }; + 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; + comment.LikeCount++; + existingLike.CreatedAt = DateTime.UtcNow; + _unitOfWork.Comments.Update(comment); + await _unitOfWork.SaveAsync(); + await transaction.CommitAsync(); + + _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 + return await ServiceResult.Success(new CommentLikeDto + { + CommentId = existingLike.CommentId, + UserId = existingLike.UserId, + UserName = userName, + CreatedAt = existingLike.CreatedAt + }); + } + + var newLike = new CommentLike + { + CommentId = commentId, + UserId = userId, + }; + + await _unitOfWork.CommentLikes.AddAsync(newLike); + + comment.LikeCount++; + _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 + { + CommentId = newLike.CommentId, + UserId = newLike.UserId, + UserName = userName, + CreatedAt = newLike.CreatedAt + }); + } + 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}); + } + + } + + + 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); + + var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); + + if (comment == null) + { + var errors = new List() + { + "$User didn't like this comment" + }; + await transaction.RollbackAsync(); + return await ServiceResult.Failure(errors); + } + + + 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) + { + var errors = new List() + { + "User didn't like this comment" + + }; + await transaction.RollbackAsync(); + return await ServiceResult.Failure(errors); + } + + await _unitOfWork.CommentLikes.RemoveAsync(commentLike); + + + if (comment.CommentLikes != null) + comment.CommentLikes.Remove(commentLike); + + if (comment.LikeCount > 0) + comment.LikeCount--; + + _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}); + } + } +} \ 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..6400a88 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/CommentService.cs @@ -0,0 +1,231 @@ +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 readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public CommentService(IUnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + public async Task> GetCommentAsync(int id) + { + try + { + 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) + { + _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" }); + } + + + int skip = (page - 1) * pageSize; + // Get paginated comments + var comments = await _unitOfWork.Comments.GetPagedAsync( + 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 = trimmed.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, + HasMore = hasMore + }; + + 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/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/ICommentLikeService.cs b/AskFm/AskFm.BLL/Services/ICommentLikeService.cs new file mode 100644 index 0000000..5285cd5 --- /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..8c58275 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ICommentService.cs @@ -0,0 +1,19 @@ +using AskFm.BLL.DTO; +using AskFm.DAL.Models; + +namespace AskFm.BLL.Services; + +public interface ICommentService +{ + // 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/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/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs new file mode 100644 index 0000000..afa49ad --- /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>> 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/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/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/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs new file mode 100644 index 0000000..d329412 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IThreadService.cs @@ -0,0 +1,17 @@ +using AskFm.BLL.DTO; + +namespace AskFm.BLL.Services; + +public interface IThreadService +{ + 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/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs new file mode 100644 index 0000000..cd3c90b --- /dev/null +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -0,0 +1,210 @@ +using AskFm.BLL.DTO; +using AskFm.BLL.Hub; +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; + _unitOfWork = unitOfWork; + _hubContext = hubContext; + } + + public async Task>> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) + { + try + { + var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, 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(), + 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 }); + } + } + + public async Task>> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) + { + 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 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(), + 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 }); + } + } + + public async Task> MarkNotificationAsRead(int notificationId, int userId) + { + 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(); + + 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) + { + try + { + 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 await ServiceResult.Success("All notifications marked as read"); + } + catch (Exception ex) + { + return await ServiceResult.Failure(new List { ex.Message }); + } + } + + public async Task> CreateNotification(int userId, NotificationStatus type, int resourceId, string message) + { + try + { + 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 + { + 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); + + 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/AskFm.BLL/Services/RedisCacheService.cs b/AskFm/AskFm.BLL/Services/RedisCacheService.cs new file mode 100644 index 0000000..0c95a83 --- /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 : IRedisService +{ + 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); + } +} + diff --git a/AskFm/AskFm.BLL/Services/ServiceResult.cs b/AskFm/AskFm.BLL/Services/ServiceResult.cs new file mode 100644 index 0000000..74c8c58 --- /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 async Task> Success(T data) + { + return new ServiceResult + { + success = true, + Data = data + }; + } + public static async Task> Success() + { + return new ServiceResult + { + success = true, + }; + } + + public static async Task> Failure(List errors) + { + return new ServiceResult + { + success = false, + Errors = errors + }; + } +} \ 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..27facb4 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs @@ -0,0 +1,203 @@ +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, IMapper mapper) + { + _unitOfWork = unitOfWork; + _logger = logger; + _mapper = mapper; + } + + // 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; + existingLike.CreatedAt = DateTime.UtcNow; + 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 + 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.UtcNow + }; + + // 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 new file mode 100644 index 0000000..77cb2a7 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -0,0 +1,570 @@ +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; + +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, IMapper mapper) + { + _unitOfWork = unitOfWork; + _logger = logger; + _mapper = mapper; + } + + 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.UtcNow, + }; + + 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(); + + await transaction.CommitAsync(); + + 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.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = askedUser.Name + }; + + return await ServiceResult.Success(responseDto); + } + catch (Exception e) + { + _logger.LogError(e, "Error adding thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task> GetThreadById(int id) + { + try + { + var thread = await _unitOfWork.Threads.FindAsync( + predicate: t => t.Id == id && !t.IsDeleted, + 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 (threads == null || !threads.Any()) + { + return await ServiceResult>.Success(new List()); + } + + 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(threadDtos); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving threads"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> AnswerThread(int threadId, int userId, + AnswerThreadDto answerDto) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + 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" } + ); + + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + 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(); + + 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, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0, + AskedName = thread.Asked?.Name + }; + + 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 + 1, + t => t.CreatedAt, + false, + t => true, + new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); + + bool hasMore = threads.Count > pageSize; + + var trimmed = threads.Take(pageSize).ToList(); + + var threadDtos = trimmed.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, + HasMore = hasMore + + }; + + 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 + 1, + t => t.CreatedAt, + false, + t => followedUserIds.Contains(t.AskedId) && t.Status == ThreadStatus.Answered, + new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); + + // 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, + 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, + HasMore = hasMore + }; + + 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.UtcNow + }; + + // 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 + 1, + st => st.CreatedAt, + false, + st => st.UserId == userId, + new[] { "Thread", "Thread.Asker", "Thread.Asked", "Thread.Comments", "Thread.ThreadLikes" } + ); + + + 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, + 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, + HasMore = hasMore + }; + + 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.BLL/Services/UserIdentityService/AuthService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs new file mode 100644 index 0000000..e7a78c5 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs @@ -0,0 +1,318 @@ +using System.IdentityModel.Tokens.Jwt; +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; +using AskFm.DAL.Models; +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; +using Microsoft.IdentityModel.Tokens; +using Shared; +namespace AskFm.BLL.Services.UserIdentityService; + +public class AuthService : IAuthService +{ + private IUnitOfWork _unitOfWork; + private readonly UserManager _userManager; + 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, IEmailSender emailSender) + { + _unitOfWork = unitOfWork; + _userManager = userManager; + _redisCacheService = redisCacheService; + _configuration = configuration; + _emailSender = emailSender; + _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); + } + + 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) + { + var errors = new List { "Invalid Email or Password." }; + return await ServiceResult.Failure(errors); + } + + var response = await GetAuthToken(getUser); + return response; + + } + + public async Task> RegisterAsync(RegisterUserDTO request) + { + 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 && oldUser.IsDeleted) + { + 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 async Task> RefreshTokenAsync(int id, string refreshToken) + { + 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); + } + + 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); + } + await _redisCacheService.RemoveCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); + return await GetAuthToken(user); + } + + + public async Task> RevokeRefreshTokenAsync(int id, string refreshToken) + { + if (string.IsNullOrEmpty(refreshToken)) + { + var errors = new List { "Invalid Token." }; + return await ServiceResult.Failure(errors); + } + + var user = await _unitOfWork.Users.GetByIdAsync(id); + if (user == null) + { + 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) + { + var errors = new List { "Invalid Token." }; + return await ServiceResult.Failure(errors); + } + + 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); + } + + 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 + 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); + } + var oldJwtId = await _redisCacheService.GetCacheAsync(AppConstants.UserJwtCacheKey(user.Id)); + if (!string.IsNullOrEmpty(oldJwtId)) + { + 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)); + + //---------------------------------- + // Use the exist refreshToken or regenerate one + + var refreshToken = await _redisCacheService.GetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id)); + if (refreshToken == null) + { + refreshToken = await generateRefreshToken(); + await _redisCacheService.SetCacheAsync(AppConstants.UserRefreshTokenCacheKey(user.Id), + refreshToken, TimeSpan.FromDays(refreshToken.ExpireAfter)); + } + + 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 RefreshTokenDto + { + Token = Convert.ToBase64String(randomNumber), + 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, string jti) + { + 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()), + 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 new file mode 100644 index 0000000..fe1cda3 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs @@ -0,0 +1,15 @@ +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(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 new file mode 100644 index 0000000..ba4f9bf --- /dev/null +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs @@ -0,0 +1,25 @@ +using AskFm.BLL.DTO.UserDTOs; +using AskFm.DAL.Models; + +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); + Task> GetUserByIdAsync(int userId); + Task> GetCurrentUserAsync(); + Task> UpdatePassword(int userId, UpdatePasswordDTO updatePasswordDto); + Task> ResetEmail(int userId, string updatedEmail); + + + /* +GET Users only for now + + confirm email + Helper Function: getCurrentUserId +*/ +} diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs new file mode 100644 index 0000000..0782b19 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs @@ -0,0 +1,10 @@ +namespace AskFm.BLL.Services.UserIdentityService; + +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 new file mode 100644 index 0000000..fdbf633 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -0,0 +1,279 @@ +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; +using Microsoft.EntityFrameworkCore; + +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 async Task> UpdateUserAsync(int userId, UpdateUserDTO updatedUser) + { + 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 async Task> DeleteUserAsync(int userId) + { + var appUser = await _unitOfWork.Users.GetByIdAsync(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 async Task> FollowUserAsync(int followerId, int targetUserId) + { + + 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 userFollowerNullRes = await CheckNullObjectAsync(userFollower); + if (!userFollowerNullRes.success) return userFollowerNullRes; + + var targetUser = await _unitOfWork.Users.GetByIdAsync(targetUserId); + var targetUserNullRes = await CheckNullObjectAsync(targetUser); + if (!targetUserNullRes.success) return targetUserNullRes; + + 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 async Task> UnfollowUserAsync(int followerId, int targetUserId) + { + 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 userFollowerNullRes = await CheckNullObjectAsync(userFollower); + if (!userFollowerNullRes.success) return userFollowerNullRes; + + var targetUser = await _unitOfWork.Users.GetByIdAsync(targetUserId); + var targetUserNullRes = await CheckNullObjectAsync(targetUser); + if (!targetUserNullRes.success) return targetUserNullRes; + + + var followExist = await _unitOfWork.Follows.GetAll() + .FirstOrDefaultAsync(f => f.FollowedId == targetUserId + && f.FollowerId == followerId && !f.IsDeleted); + + 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(); + + return await ServiceResult.Success(true); + } + catch (Exception) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Invalid unfollow operation" }); + } + } + + public async Task> UpdateLastSeenAsync(int userId) + { + 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.UtcNow; + await _unitOfWork.Users.UpdateAsync(appUser); + await _unitOfWork.SaveAsync(); + await transaction.CommitAsync(); + return await ServiceResult.Success(true); + } + catch (Exception) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Invalid update operation" }); + } + + } + + public async Task> GetUserByIdAsync(int userId) + { + var user = await _unitOfWork.Users.GetByIdAsync(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() + { + 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(currentAppUser); + } + + 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, updatePasswordDto.CurrentPassword); + if (!passwordValid) + { + var errors = new List { "Invalid Password." }; + return await ServiceResult.Failure(errors); + } + 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, updatePasswordDto.CurrentPassword, updatePasswordDto.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.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 diff --git a/AskFm/AskFm.DAL/AppDbContext.cs b/AskFm/AskFm.DAL/AppDbContext.cs index 70b80cd..158cc1e 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; } @@ -23,5 +26,66 @@ 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") + .HasDefaultValueSql("GETUTCDATE()"); + + modelBuilder.Entity(entityType.ClrType).Property("UpdatedAt") + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + modelBuilder.Entity(entityType.ClrType).Property("CreatedAt") + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + modelBuilder.Entity(entityType.ClrType).Property("IsDeleted") + .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); + + } + + 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 diff --git a/AskFm/AskFm.DAL/AskFm.DAL.csproj b/AskFm/AskFm.DAL/AskFm.DAL.csproj index 4b32852..e33b6a5 100644 --- a/AskFm/AskFm.DAL/AskFm.DAL.csproj +++ b/AskFm/AskFm.DAL/AskFm.DAL.csproj @@ -1,35 +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 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/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..310ec78 --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -0,0 +1,12 @@ +using AskFm.DAL.Enums; +using AskFm.DAL.Models; + +namespace AskFm.DAL.Interfaces; + +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/Interfaces/IRepository.cs b/AskFm/AskFm.DAL/Interfaces/IRepository.cs new file mode 100644 index 0000000..469cd08 --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/IRepository.cs @@ -0,0 +1,45 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; + +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); + + 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/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..042eedf --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs @@ -0,0 +1,22 @@ +using AskFm.DAL.Models; +using Microsoft.EntityFrameworkCore.Storage; +using Thread = AskFm.DAL.Models.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 Save(); + Task SaveAsync(); + + Task BeginTransactionAsync(); +} \ 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..20cf2e5 --- /dev/null +++ b/AskFm/AskFm.DAL/Interfaces/IUserRepository.cs @@ -0,0 +1,9 @@ +using AskFm.DAL.Models; + +namespace AskFm.DAL.Interfaces; + +public interface IApplicationUserRepository : IRepository +{ + Task GetByUsernameAsync(string username); + +} \ No newline at end of file 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"); + } + } +} 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/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/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/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/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/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/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/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 9b959ec..6554b8b 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -18,10 +18,130 @@ 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); + 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") @@ -36,7 +156,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(1000)"); b.Property("CreatedAt") - .HasColumnType("datetime"); + .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"); @@ -47,6 +179,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ThreadId") .HasColumnType("int"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + b.Property("UserId") .HasColumnType("int"); @@ -70,7 +207,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("datetime"); + .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"); @@ -88,10 +242,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("datetime2"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsActive") - .HasColumnType("bit"); + .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"); @@ -109,21 +282,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("datetime"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); - b.Property("ResourceId") - .HasColumnType("int"); + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); - b.Property("UserId") - .HasColumnType("int"); + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); - b.Property("isRead") + b.Property("IsRead") .HasColumnType("bit"); - b.Property("jsonContent") + 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"); @@ -139,6 +332,26 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); @@ -166,7 +379,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("datetime"); + .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() @@ -177,6 +402,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + b.Property("isAnonymous") .HasColumnType("bit"); @@ -198,7 +428,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("datetime"); + .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"); @@ -207,7 +454,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() @@ -215,67 +462,145 @@ 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("Email") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("nvarchar(255)"); + b.HasKey("Id"); - b.Property("FollowersCount") + 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"); - 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("LastSeen") - .HasColumnType("datetime2"); + b.HasKey("Id"); - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + b.HasIndex("RoleId"); - b.Property("Password") - .IsRequired() - .HasMaxLength(225) - .HasColumnType("nvarchar(225)"); + b.ToTable("AspNetRoleClaims", (string)null); + }); - b.Property("Username") - .IsRequired() - .HasColumnType("nvarchar(450)"); + 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("Username") - .IsUnique(); + 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("Users"); + 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"); + .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") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") .WithMany("Comments") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.NoAction); @@ -295,10 +620,10 @@ 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.Cascade) + .OnDelete(DeleteBehavior.NoAction) .IsRequired(); b.Navigation("Comment"); @@ -308,13 +633,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) @@ -327,10 +652,10 @@ 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.Cascade) + .OnDelete(DeleteBehavior.NoAction) .IsRequired(); b.Navigation("User"); @@ -344,7 +669,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) @@ -357,15 +682,16 @@ 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.Cascade) + .OnDelete(DeleteBehavior.NoAction) .IsRequired(); - b.HasOne("AskFm.DAL.Models.User", "Asker") + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") .WithMany("AskedThreads") - .HasForeignKey("AskerId"); + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); b.Navigation("Asked"); @@ -380,10 +706,10 @@ 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.Cascade) + .OnDelete(DeleteBehavior.NoAction) .IsRequired(); b.Navigation("Thread"); @@ -391,23 +717,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("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); - modelBuilder.Entity("AskFm.DAL.Models.User", b => + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => { b.Navigation("AskedThreads"); @@ -425,6 +786,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 77% rename from AskFm/AskFm.DAL/Models/User.cs rename to AskFm/AskFm.DAL/Models/ApplicationUser.cs index 26ebe03..2b7655c 100644 --- a/AskFm/AskFm.DAL/Models/User.cs +++ b/AskFm/AskFm.DAL/Models/ApplicationUser.cs @@ -1,19 +1,17 @@ +using System.Runtime.InteropServices.JavaScript; +using Microsoft.AspNetCore.Identity; + namespace AskFm.DAL.Models; -public class User +public class ApplicationUser : IdentityUser, ITrackable { - 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; } 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; } @@ -25,5 +23,10 @@ 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; } + 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 b4576ba..b936b24 100644 --- a/AskFm/AskFm.DAL/Models/Comment.cs +++ b/AskFm/AskFm.DAL/Models/Comment.cs @@ -1,14 +1,16 @@ +using System.Runtime.InteropServices.JavaScript; +using Microsoft.AspNetCore.Identity; + namespace AskFm.DAL.Models; -public class Comment +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; } - public virtual User? User { get; set; } + public virtual ApplicationUser? User { get; set; } public int ThreadId { get; set; } public virtual Thread? Thread { get; set; } @@ -18,4 +20,8 @@ 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; } + 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 1bc9030..b75e030 100644 --- a/AskFm/AskFm.DAL/Models/CommentLike.cs +++ b/AskFm/AskFm.DAL/Models/CommentLike.cs @@ -1,10 +1,15 @@ +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 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/Follow.cs b/AskFm/AskFm.DAL/Models/Follow.cs index 639b05c..af28686 100644 --- a/AskFm/AskFm.DAL/Models/Follow.cs +++ b/AskFm/AskFm.DAL/Models/Follow.cs @@ -1,14 +1,21 @@ +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; } + public virtual ApplicationUser? Follower { get; set; } public int FollowedId { get; set; } - public virtual User? Followed { get; set; } - public DateTime CreatedAt { get; set; } + 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; } + 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 new file mode 100644 index 0000000..ec0dd36 --- /dev/null +++ b/AskFm/AskFm.DAL/Models/ITrackable.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices.JavaScript; + +namespace AskFm.DAL.Models; + +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 5e1cedf..2807d59 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -1,14 +1,21 @@ +using System.Runtime.InteropServices.JavaScript; +using AskFm.DAL.Enums; + namespace AskFm.DAL.Models; -public class Notification +public class Notification : ITrackable { - public GCNotificationStatus Type; + public NotificationStatus Type { get; set; } public int Id { get; set; } public int UserId { get; set; } - public virtual User? User { get; set; } - public bool isRead { get; set; } + public virtual ApplicationUser? User { 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; } + 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 01189aa..d856d68 100644 --- a/AskFm/AskFm.DAL/Models/SavedThreads.cs +++ b/AskFm/AskFm.DAL/Models/SavedThreads.cs @@ -1,10 +1,16 @@ +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 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 019f533..949b163 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; } @@ -9,14 +10,17 @@ public class Thread public ThreadStatus Status { get; set; } 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; } 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 cbf62c7..a1ad625 100644 --- a/AskFm/AskFm.DAL/Models/ThreadLike.cs +++ b/AskFm/AskFm.DAL/Models/ThreadLike.cs @@ -1,12 +1,17 @@ +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; } public int UserId { get; set; } - public virtual User? User { get; set; } + 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/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) diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs index 0bc7fd6..a02f7ef 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/CommentConfigration.cs @@ -14,20 +14,16 @@ 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) .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..b8b8e9d 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/CommentLikeConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/CommentLikeConfigration.cs @@ -9,15 +9,11 @@ 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) - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.NoAction); builder.HasOne(cl => cl.Comment) .WithMany(c => c.CommentLikes) 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 diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs index 4c11b35..0423d4f 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs @@ -10,19 +10,16 @@ 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) - .IsRequired(); - - builder.Property(n => n.CreatedAt) - .HasColumnType("datetime") + builder.Property(n => n.IsRead) .IsRequired(); + 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..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(); @@ -32,12 +29,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..1c6e96c 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/ThreadLikeConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/ThreadLikeConfigration.cs @@ -9,15 +9,11 @@ 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) - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.NoAction); builder.HasOne(tl => tl.Thread) .WithMany(t => t.ThreadLikes) 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..56189c6 --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -0,0 +1,86 @@ +using AskFm.DAL.Enums; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using Microsoft.EntityFrameworkCore; + +namespace AskFm.DAL.Repositories; + +public class NotificationRepository : INotificationRepository +{ + private readonly IUnitOfWork _unitOfWork; + + 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); + } + + public async Task GetActorUserByResourceId(int resourceId, NotificationStatus type) + { + if (type == NotificationStatus.FOLLOW) + { + var follow = await _unitOfWork.Follows.FindAsync(f => f.FollowedId == resourceId, new[] { "Follower" }); + return follow?.Follower; + } + else if (type == NotificationStatus.QUESTION) + { + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == resourceId, new[] { "Asker" }); + return thread?.Asker; + } + else if (type == NotificationStatus.ANSWER) + { + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == resourceId, new[] { "Asked" }); + return thread?.Asked; + } + else if (type == NotificationStatus.COMMENT_LIKE) + { + var commentLike = await _unitOfWork.CommentLikes.FindAsync(cl => cl.CommentId == resourceId, new[] { "User" }); + return commentLike?.User; + } + else if (type == NotificationStatus.QUESTION_LIKE) + { + var threadLike = await _unitOfWork.ThreadLikes.FindAsync(tl => tl.ThreadId == resourceId, new[] { "User" }); + return threadLike?.User; + } + else if (type == NotificationStatus.REPLAY) + { + var comment = await _unitOfWork.Comments.FindAsync(c => c.Id == resourceId, new[] { "User" }); + return comment?.User; + } + 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/AskFm.DAL/Repositories/Repository.cs b/AskFm/AskFm.DAL/Repositories/Repository.cs new file mode 100644 index 0000000..1a3a7c1 --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/Repository.cs @@ -0,0 +1,154 @@ +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, string[] includes = null) + { + IQueryable query = _dbSet; + + 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.FirstOrDefault(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; + } + + 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) + { + 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) + { + 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 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..587003c --- /dev/null +++ b/AskFm/AskFm.DAL/Repositories/UserRepository.cs @@ -0,0 +1,20 @@ +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using Microsoft.EntityFrameworkCore; + +namespace AskFm.DAL.Repositories; + +public class UserRepository : Repository, IApplicationUserRepository +{ + 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..c372b54 --- /dev/null +++ b/AskFm/AskFm.DAL/UnitOfWork.cs @@ -0,0 +1,144 @@ +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; + +public class UnitOfWork : IUnitOfWork +{ + private readonly AppDbContext _context; + + 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; + } + 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 Save() + { + return _context.SaveChanges(); + } + + public async Task SaveAsync() + { + return await _context.SaveChangesAsync(); + } + + public async Task BeginTransactionAsync() + { + return await _context.Database.BeginTransactionAsync(); + } + +} \ No newline at end of file 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/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 diff --git a/AskFm/Tests/CommentLikeServiceTest.cs b/AskFm/Tests/CommentLikeServiceTest.cs new file mode 100644 index 0000000..c022ccf --- /dev/null +++ b/AskFm/Tests/CommentLikeServiceTest.cs @@ -0,0 +1,261 @@ +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; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore.Storage; + + +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; + private readonly Mock _mockTransaction; + + public CommentLikeServiceTest() + { + _mockUnitOfWork = new Mock(); + _mockCommentRepository = new Mock>(); + _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); + } + + [Fact] + public async Task AddLike_WhenCommentIsNotDeleted_AddingNewLikeOnTheComment() + { + // Arrange + var commentId = 1; + var userId = 5; + var mockTransaction = new Mock(); + + 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 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); + + 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); + + } + + + [Fact] + public async Task 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 + await _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 Task 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.True(result.success); + Assert.NotNull(result.Data); + Assert.Equal(5, result.Data.Count()); + + Assert.Equivalent(expectedDtos, result.Data); + + } + + + [Fact] + public async Task 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.Data); + + } + + + // 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/NotificationServiceTests.cs b/AskFm/Tests/NotificationServiceTests.cs new file mode 100644 index 0000000..9a701ef --- /dev/null +++ b/AskFm/Tests/NotificationServiceTests.cs @@ -0,0 +1,335 @@ +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; +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); + + // 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, + _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.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] + 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.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] + 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.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_ReturnsFailureResult() + { + // Arrange + int userId = 1; + string category = "InvalidCategory"; + int pageNumber = 1; + int pageSize = 10; + + // 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] + public async Task MarkNotificationAsRead_WithValidId_UpdatesNotification() + { + // Arrange + int notificationId = 1; + 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, userId); + + // Assert + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.Equal("notification has been read", result.Data); + Assert.True(notification.IsRead); + _unitOfWorkMock.Verify(p => p.Notifications.Update(notification), Times.Once); + _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + } + + [Fact] + public async Task MarkNotificationAsRead_WithInvalidId_ReturnsFailureResult() + { + // Arrange + var notificationId = 999; + var userId = 1; + _notificationRepositoryMock.Setup(p => p.GetUserNotificationById(notificationId, userId)) + .ReturnsAsync((Notification)null); + + // Act + var result = await _notificationService.MarkNotificationAsRead(notificationId, userId); + + // 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] + public async Task MarkAllNotificationsAsRead_UpdatesAllUnreadNotifications() + { + // Arrange + int userId = 1; + var unreadNotifications = new List + { + 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>>(), null)) + .ReturnsAsync(unreadNotifications); + _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); + + // Act + var result = await _notificationService.MarkAllNotificationsAsRead(userId); + + // Assert + 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); + } + + [Fact] + public async Task CreateNotification_CreatesAndSendsNotification() + { + // Arrange + 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())); + _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); + _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(resourceId, type)) + .ReturnsAsync(actorUser); + + // Act + 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); + } + } +} \ No newline at end of file diff --git a/AskFm/Tests/Tests.csproj b/AskFm/Tests/Tests.csproj index d3517ca..aab5fe3 100644 --- a/AskFm/Tests/Tests.csproj +++ b/AskFm/Tests/Tests.csproj @@ -2,7 +2,6 @@ net9.0 - latest enable enable false @@ -10,14 +9,21 @@ + - - - + + + - + + + + + + + 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 diff --git a/AskFm/Tests/UserTests.cs b/AskFm/Tests/UserTests.cs new file mode 100644 index 0000000..85c32c0 --- /dev/null +++ b/AskFm/Tests/UserTests.cs @@ -0,0 +1,201 @@ +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() + { + // 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); + + } + + /// + /// Update DeleteUserAsync with 2 conditions + /// + [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); + + + } + + + /// + /// Follow 6, search how to test transactions + /// + /// + /// + + // 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