diff --git a/Attributes/AllowedFileExtensionsAttribute.cs b/Attributes/AllowedFileExtensionsAttribute.cs new file mode 100644 index 0000000..3d2c06e --- /dev/null +++ b/Attributes/AllowedFileExtensionsAttribute.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace AonFreelancing.Attributes +{ + public class AllowedFileExtensionsAttribute : ValidationAttribute + { + private readonly string[] _extensions; + private string _extension; + public AllowedFileExtensionsAttribute(string[] extensions) + { + _extensions = extensions; + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + + if (value is not IFormFile file) + return ValidationResult.Success; + + _extension = Path.GetExtension(file.FileName); + if (Utilities.FileCheckUtil.IsValidFileExtensionAndSignature(file.FileName, file.OpenReadStream(), _extensions)) + return ValidationResult.Success; + + return new ValidationResult(GetErrorMessage()); + } + + public string GetErrorMessage() + { + return $"either file extension ({_extension}) is not allowed or the file has been corrupted"; + } + } +} \ No newline at end of file diff --git a/Attributes/MaxFileSizeAttribute.cs b/Attributes/MaxFileSizeAttribute.cs new file mode 100644 index 0000000..bb2963a --- /dev/null +++ b/Attributes/MaxFileSizeAttribute.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace AonFreelancing.Attributes +{ + public class MaxFileSizeAttribute : ValidationAttribute + { + private readonly int _maxFileSize; + public MaxFileSizeAttribute(int maxFileSize) + { + _maxFileSize = maxFileSize; + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var file = value as IFormFile; + if (file != null && file.Length > _maxFileSize) + return new ValidationResult(GetErrorMessage()); + + return ValidationResult.Success; + } + + public string GetErrorMessage() + { + return $"Maximum allowed file size is {_maxFileSize} bytes."; + } + } +} \ No newline at end of file diff --git a/Contexts/MainAppContext.cs b/Contexts/MainAppContext.cs index fac806e..ce364f2 100644 --- a/Contexts/MainAppContext.cs +++ b/Contexts/MainAppContext.cs @@ -1,4 +1,5 @@ using AonFreelancing.Models; +using AonFreelancing.Utilities; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using System.Reflection.Emit; @@ -20,7 +21,8 @@ public class MainAppContext(DbContextOptions contextOptions) public DbSet TempUsers { get; set; } public DbSet Bids { get; set; } public DbSet Tasks { get; set; } - + public DbSet Skills { get; set; } + public DbSet ProjectLikes { get; set; } protected override void OnModelCreating(ModelBuilder builder) { @@ -33,21 +35,24 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity().ToTable("Freelancers"); builder.Entity().ToTable("Clients"); builder.Entity().ToTable("SystemUsers"); + builder.Entity().ToTable("otps", o => o.HasCheckConstraint("CK_CODE","LEN([Code]) = 6")); + builder.Entity().ToTable("Projects", tb => tb.HasCheckConstraint("CK_PRICE_TYPE", "[PriceType] IN ('Fixed', 'PerHour')")); + builder.Entity().ToTable("Projects", tb => tb.HasCheckConstraint("CK_QUALIFICATION_NAME", "[QualificationName] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')")); + builder.Entity().ToTable("Projects", tb => tb.HasCheckConstraint("CK_STATUS", "[Status] IN ('Available', 'Closed')")) + .Property(p=>p.Status).HasDefaultValue("Available"); + + builder.Entity().ToTable("Tasks", t => t.HasCheckConstraint("CK_TASK_STATUS", $"[Status] IN ('{Constants.TASK_STATUS_DONE}', '{Constants.TASK_STATUS_IN_REVIEW}', '{Constants.TASK_STATUS_IN_PROGRESS}', '{Constants.TASK_STATUS_TO_DO}')")) + .Property(t => t.Status).HasDefaultValue(Constants.TASK_STATUS_TO_DO); + + builder.Entity().HasIndex(pl => new { pl.ProjectId, pl.UserId }).IsUnique(); //set up relationships builder.Entity().HasOne() .WithOne() .HasForeignKey() .HasPrincipalKey(nameof(TempUser.PhoneNumber)); - builder.Entity().ToTable("Projects", tb => tb.HasCheckConstraint("CK_PRICE_TYPE", "[PriceType] IN ('Fixed', 'PerHour')")); - - builder.Entity() - .ToTable("Projects", tb => tb.HasCheckConstraint("CK_QUALIFICATION_NAME", "[QualificationName] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')")); - builder.Entity().ToTable("Projects", tb => tb.HasCheckConstraint("CK_STATUS", "[Status] IN ('Available', 'Closed')")) - .Property(p=>p.Status).HasDefaultValue("Available"); - builder.Entity() .HasOne(b => b.Project) .WithMany(p => p.Bids) @@ -60,8 +65,21 @@ protected override void OnModelCreating(ModelBuilder builder) .HasForeignKey(b => b.FreelancerId) .OnDelete(DeleteBehavior.NoAction); + builder.Entity().HasOne() + .WithMany(f=>f.Skills) + .HasForeignKey(s=>s.UserId) + .HasPrincipalKey(f=>f.Id); + + builder.Entity().HasOne() + .WithMany() + .HasForeignKey(pl => pl.UserId) + .HasPrincipalKey(u => u.Id); + builder.Entity().HasOne() + .WithMany(p => p.ProjectLikes) + .HasForeignKey(pl => pl.ProjectId) + .OnDelete(DeleteBehavior.NoAction) + .HasPrincipalKey(p => p.Id); - builder.Entity().ToTable("Tasks"); base.OnModelCreating(builder); } diff --git a/Controllers/BaseController.cs b/Controllers/BaseController.cs index 28809e3..5fa2748 100644 --- a/Controllers/BaseController.cs +++ b/Controllers/BaseController.cs @@ -1,4 +1,4 @@ -using AonFreelancing.Models; +using AonFreelancing.Models.Responses; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; diff --git a/Controllers/Mobile/v1/AuthController.cs b/Controllers/Mobile/v1/AuthController.cs index 007317b..e7b88bb 100644 --- a/Controllers/Mobile/v1/AuthController.cs +++ b/Controllers/Mobile/v1/AuthController.cs @@ -43,6 +43,8 @@ AuthService authService [HttpPost("sendVerificationCode")] public async Task SendVerificationCodeAsync([FromBody] PhoneNumberReq phoneNumberReq) { + if (!ModelState.IsValid) + return CustomBadRequest(); var IsExist = await _authService.IsUserExistsInTempAsync(phoneNumberReq); if (IsExist) { @@ -110,7 +112,7 @@ public async Task CompleteRegistrationAsync([FromBody] RegisterRe UserName = registerReq.Username, PhoneNumber = tempUser.PhoneNumber, PhoneNumberConfirmed = tempUser.PhoneNumberConfirmed, - Skills = registerReq.Skills ?? string.Empty, + //Skills = registerReq.Skills ?? string.Empty, }, Constants.USER_TYPE_CLIENT => new Client() { diff --git a/Controllers/Mobile/v1/ProjectsController.cs b/Controllers/Mobile/v1/ProjectsController.cs index d140b73..4094f59 100644 --- a/Controllers/Mobile/v1/ProjectsController.cs +++ b/Controllers/Mobile/v1/ProjectsController.cs @@ -2,324 +2,278 @@ using AonFreelancing.Contexts; using AonFreelancing.Models; using AonFreelancing.Models.DTOs; +using AonFreelancing.Models.Responses; +using AonFreelancing.Services; using AonFreelancing.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; namespace AonFreelancing.Controllers.Mobile.v1 { [Authorize] [Route("api/mobile/v1/projects")] [ApiController] - public class ProjectsController(MainAppContext mainAppContext, UserManager userManager) : BaseController + public class ProjectsController(MainAppContext mainAppContext, FileStorageService fileStorageService, UserManager userManager, ProjectLikeService projectLikeService, AuthService authService) : BaseController { [Authorize(Roles = "CLIENT")] [HttpPost] - public async Task PostProjectAsync([FromBody] ProjectInputDto projectInputDto) + public async Task PostProjectAsync([FromForm] ProjectInputDto projectInputDto) { - if(!ModelState.IsValid) - { - + if (!ModelState.IsValid) return base.CustomBadRequest(); - } - var user = await userManager.GetUserAsync(HttpContext.User); - if (user == null) + + User? authenticatedUser = await userManager.GetUserAsync(HttpContext.User); + if (authenticatedUser == null) return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "Unable to load user.")); - var userId = user.Id; - var project = new Project - { - ClientId = userId, - Title = projectInputDto.Title, - Description = projectInputDto.Description, - QualificationName = projectInputDto.QualificationName, - Duration = projectInputDto.Duration, - Budget = projectInputDto.Budget, - PriceType = projectInputDto.PriceType, - CreatedAt = DateTime.Now, - }; - - await mainAppContext.Projects.AddAsync(project); + long clientId = authenticatedUser.Id; + Project? newProject = Project.FromInputDTO(projectInputDto, clientId); + + if (projectInputDto.ImageFile != null) + newProject.ImageFileName = await fileStorageService.SaveAsync(projectInputDto.ImageFile); + + await mainAppContext.Projects.AddAsync(newProject); await mainAppContext.SaveChangesAsync(); - return Ok(CreateSuccessResponse("Project added.")); + return CreatedAtAction(nameof(GetProjectDetailsAsync),new {id = newProject.Id},null); } - [Authorize(Roles = "CLIENT")] - [HttpGet("clientFeed")] + [Authorize(Roles = Constants.USER_TYPE_CLIENT)] + [HttpGet("clientfeed")] public async Task GetClientFeedAsync( [FromQuery] List? qualificationNames, [FromQuery] int page = 0, - [FromQuery] int pageSize = 8, [FromQuery] string? qur = default + [FromQuery] int pageSize = 8, [FromQuery] string qur = "" ) { - var trimmedQuery = qur?.ToLower().Replace(" ", "").Trim(); - List? projects; - - var query = mainAppContext.Projects.AsQueryable(); + string imagesBaseUrl = $"{Request.Scheme}://{Request.Host}/images"; + string normalizedQuery = qur.ToLower().Replace(" ", "").Trim(); + List? storedProjects; + var query = mainAppContext.Projects.AsNoTracking().Include(p => p.Client).Include(p => p.ProjectLikes).AsQueryable(); + int totalProjectsCount = await query.CountAsync(); - var count = await query.CountAsync(); + if (!string.IsNullOrEmpty(normalizedQuery)) + query = query.Where(p => p.Title.ToLower().Contains(normalizedQuery)); - if(!string.IsNullOrEmpty(trimmedQuery)) - { - query = query - .Where(p=>p.Title.ToLower().Contains(trimmedQuery)); - } - if(qualificationNames != null && qualificationNames.Count >0) - { - query = query - .Where(p => qualificationNames.Contains(p.QualificationName)); - } + if (qualificationNames != null && qualificationNames.Count > 0) + query = query.Where(p => qualificationNames.Contains(p.QualificationName)); - projects = await query.OrderByDescending(p => p.CreatedAt) + storedProjects = await query.OrderByDescending(p => p.CreatedAt) .Skip(page * pageSize) .Take(pageSize) - .Select(p => new ProjectOutDTO - { - Id= p.Id, - Title = p.Title, - Description = p.Description, - Status = p.Status, - Budget = p.Budget, - Duration = p.Duration, - PriceType = p.PriceType, - Qualifications = p.QualificationName, - StartDate = p.StartDate, - EndDate = p.EndDate, - CreatedAt = p.CreatedAt, - CreationTime = StringOperations.GetTimeAgo(p.CreatedAt) - }) + .Select(p => ProjectOutDTO.FromProject(p, imagesBaseUrl)) .ToListAsync(); - - return Ok(CreateSuccessResponse(new { - Total=count, - Items=projects - })); + + return Ok(CreateSuccessResponse(new PaginatedResult(totalProjectsCount, storedProjects))); + } + + [Authorize(Roles = Constants.USER_TYPE_FREELANCER)] + [HttpGet("freelancerfeed")] + public async Task GetProjectFeedAsync( + [FromQuery(Name = "specializations")] List? qualificationNames, + [FromQuery(Name = "timeline")] int? duration, + [FromQuery] PriceRange priceRange, + [FromQuery] int page = 0, + [FromQuery] int pageSize = 8, + [FromQuery] string qur = "" + ) + { + if (!ModelState.IsValid) + return base.CustomBadRequest(); + + string imagesBaseUrl = $"{Request.Scheme}://{Request.Host}/images"; + string normalizedQuery = qur.ToLower().Replace(" ", "").Trim(); + var query = mainAppContext.Projects.AsNoTracking().Include(p => p.Client).Include(p => p.ProjectLikes).AsQueryable(); + int totalProjectsCount = await query.CountAsync(); + + if (!string.IsNullOrEmpty(normalizedQuery)) + query = query.Where(p => p.Title.ToLower().Contains(normalizedQuery)); + + if (qualificationNames != null && qualificationNames.Count > 0) + query = query.Where(p => qualificationNames.Contains(p.QualificationName)); + + if (duration.HasValue) + query = query.Where(p => p.Duration >= duration.Value); + + if (priceRange.MinPrice != null && priceRange.MaxPrice != null) + query = query.Where(p => p.Budget >= priceRange.MinPrice && p.Budget <= priceRange.MaxPrice); + + + List? storedProjects = await query.OrderByDescending(p => p.CreatedAt) + .Skip(page * pageSize) + .Take(pageSize) + .Select(p => ProjectOutDTO.FromProject(p, imagesBaseUrl)) + .ToListAsync(); + return Ok(CreateSuccessResponse(new PaginatedResult(totalProjectsCount, storedProjects))); } [Authorize(Roles = "FREELANCER")] - [HttpPost("{id}/bids")] - public async Task SubmitBidAsync(long id, [FromBody] BidInputDto bidDto) + [HttpPost("{projectId}/bids")] + public async Task SubmitBidAsync(long projectId, [FromBody] BidInputDto bidInputDTO) { if (!ModelState.IsValid) return CustomBadRequest(); - var project = await mainAppContext.Projects.FindAsync(id); - if (project == null) - return NotFound(CreateErrorResponse("404", "Project not found.")); + long authenticatedFreelancerId = authService.GetUserId((ClaimsIdentity)HttpContext.User.Identity); + Project? storedProject = mainAppContext.Projects.Where(p => p.Id == projectId).Include(p => p.Bids).FirstOrDefault(); - var user = await userManager.GetUserAsync(User); - //if (user == null || !User.IsInRole("FREELANCER")) - // return Forbid(); + if (storedProject == null) + return NotFound(CreateErrorResponse("404", "project not found")); + if (storedProject.Status != Constants.PROJECT_STATUS_AVAILABLE) + return Conflict(CreateErrorResponse("409", "cannot submit a bid for project that is not available for bids")); + if (storedProject.Budget <= bidInputDTO.ProposedPrice) + return BadRequest(CreateErrorResponse("400", "proposed price must be less than the project's budget")); + if (storedProject.Bids.Any() && storedProject.Bids.OrderBy(b => b.ProposedPrice).First().ProposedPrice <= bidInputDTO.ProposedPrice) + return BadRequest(CreateErrorResponse("40", "proposed price must be less than earlier proposed prices")); - var lastBid = await mainAppContext.Bids - .Where(b => b.ProjectId == id) - .OrderByDescending(b => b.SubmittedAt) - .FirstOrDefaultAsync(); - - if (bidDto.ProposedPrice <= 0 || - (lastBid != null && bidDto.ProposedPrice > lastBid.ProposedPrice) || - (lastBid == null && bidDto.ProposedPrice > project.Budget)) - { - return BadRequest(CreateErrorResponse("400", "Invalid proposed price. The proposed price must be positive and lower than the last bid or project budget.")); - } - - var bid = new Bid - { - ProjectId = id, - FreelancerId = user.Id, - ProposedPrice = bidDto.ProposedPrice, - Notes = bidDto.Notes, - Status = Constants.BIDS_STATUS_PENDING, - SubmittedAt = DateTime.Now - }; - - await mainAppContext.Bids.AddAsync(bid); + Bid? newBid = Bid.FromInputDTO(bidInputDTO, authenticatedFreelancerId, projectId); + await mainAppContext.AddAsync(newBid); await mainAppContext.SaveChangesAsync(); - return Ok(CreateSuccessResponse("Bid submitted successfully.")); + return StatusCode(StatusCodes.Status201Created); } [Authorize(Roles = "CLIENT")] - [HttpPut("{pid}/bids/{bid}/approve")] - public async Task ApproveBidAsync(long pid, long bid) + [HttpPut("{projectId}/bids/{bidId}/approve")] + public async Task ApproveBidAsync([FromRoute] long projectId, [FromRoute] long bidId) { - var project = await mainAppContext.Projects.FindAsync(pid); - if (project == null || project.Status != Constants.PROJECT_STATUS_AVAILABLE) - return BadRequest(CreateErrorResponse("400", $"Project status is '{project?.Status}', but must be 'available'.")); - var bidID = await mainAppContext.Bids.FirstOrDefaultAsync(b => b.Id == bid); - if (bidID == null || bidID.ProjectId != pid || bidID.Status == Constants.BIDS_STATUS_APPROVED) - return BadRequest(CreateErrorResponse("400", "Bid not found or already approved.")); + long authenticatedClientId = authService.GetUserId((ClaimsIdentity)HttpContext.User.Identity); + Project? storedProject = await mainAppContext.Projects.Where(p => p.Id == projectId) + .Include(p => p.Bids) + .FirstOrDefaultAsync(); - bidID.Status = Constants.BIDS_STATUS_APPROVED; - bidID.ApprovedAt = DateTime.Now; + if (storedProject == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "project not found")); - project.Status = Constants.PROJECT_STATUS_CLOSED; + if (authenticatedClientId != storedProject.ClientId) + return Forbid(); - await mainAppContext.SaveChangesAsync(); + if (storedProject.Status != Constants.PROJECT_STATUS_AVAILABLE) + return Conflict(CreateErrorResponse(StatusCodes.Status409Conflict.ToString(), "project status is not 'Available'")); - return Ok(CreateSuccessResponse("Bid approved successfully.")); + Bid? storedBid = storedProject.Bids.Where(b => b.Id == bidId).FirstOrDefault(); + if (storedBid == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "bid not found")); + + storedBid.Status = Constants.BIDS_STATUS_APPROVED; + storedBid.ApprovedAt = DateTime.Now; + storedProject.Status = Constants.PROJECT_STATUS_CLOSED; + storedProject.FreelancerId = storedBid.FreelancerId; + + await mainAppContext.SaveChangesAsync(); + return Ok(); } [HttpGet("{id}")] public async Task GetProjectDetailsAsync(long id) { - var project = await mainAppContext.Projects - .Where(p => p.Id == id) - .Include(p => p.Bids) - .ThenInclude(b => b.Freelancer) - .FirstOrDefaultAsync(); + var storedProject = await mainAppContext.Projects.Where(p => p.Id == id) + .Include(p => p.Tasks) + .Include(p => p.Bids) + .ThenInclude(b => b.Freelancer) + .FirstOrDefaultAsync(); - if (project == null) + if (storedProject == null) return NotFound(CreateErrorResponse("404", "Project not found.")); - var orderedBids = project.Bids + int numberOfCompletedTasks = storedProject.Tasks.Where(t => t.Status == Constants.TASK_STATUS_DONE).ToList().Count; + decimal totalNumberOFTasks = storedProject.Tasks.Count; + decimal percentage = 0; + if (totalNumberOFTasks > 0) + percentage = (numberOfCompletedTasks / totalNumberOFTasks) * 100; + + var orderedBids = storedProject.Bids .OrderByDescending(b => b.ProposedPrice) - .Select(b => new BidOutDto { - Id = b.Id, - FreelancerId = b.FreelancerId, - Freelancer = new FreelancerShortOutDTO { - Id = b.FreelancerId, - Name = b.Freelancer.Name - }, - ProposedPrice = b.ProposedPrice, - Notes = b.Notes, - Status = b.Status, - SubmittedAt = b.SubmittedAt, - ApprovedAt = b.ApprovedAt - } ); - - - + .Select(b => BidOutputDTO.FromBid(b)); + return Ok(CreateSuccessResponse(new { - project.Id, - project.Title, - project.Status, - project.Budget, - project.Duration, - project.Description, + storedProject.Id, + storedProject.Title, + storedProject.Status, + storedProject.Budget, + storedProject.Duration, + storedProject.Description, + Percentage = percentage, Bids = orderedBids })); } - - [Authorize(Roles = "CLIENT")] - [HttpPost("{id}/tasks")] - public async Task CreateTaskAsync(long id, [FromBody] TaskInputDto taskDto) + [Authorize(Roles = Constants.USER_TYPE_CLIENT)] + [HttpPost("{projectId}/tasks")] + public async Task CreateTaskAsync(long projectId, [FromBody] TaskInputDTO taskInputDTO) { - var project = await mainAppContext.Projects.FindAsync(id); - if (project == null || project.Status != Constants.PROJECT_STATUS_CLOSED) - return BadRequest(CreateErrorResponse("400", "Project not found or not closed.")); - - var task = new TaskEntity - { - ProjectId = id, - Name = taskDto.Name, - DeadlineAt = taskDto.DeadlineAt, - Notes = taskDto.Notes - }; - - await mainAppContext.Tasks.AddAsync(task); + long authenticatedClientId = authService.GetUserId((ClaimsIdentity)HttpContext.User.Identity); + Project? storedProject = await mainAppContext.Projects.AsNoTracking().FirstOrDefaultAsync(p=>p.Id == projectId); + if (storedProject == null ) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "Project not found")); + if (authenticatedClientId != storedProject.ClientId) + return Forbid(); + if(storedProject.Status != Constants.PROJECT_STATUS_CLOSED) + return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), "project is not status closed yet")); + + + TaskEntity? newTask = TaskEntity.FromInputDTO(taskInputDTO, projectId); + await mainAppContext.Tasks.AddAsync(newTask); await mainAppContext.SaveChangesAsync(); return Ok(CreateSuccessResponse("Task created successfully.")); } - - [Authorize(Roles = "CLIENT")] - [HttpPost("{id}/upload-image")] - public async Task UploadProjectImage(long id, IFormFile file) + [HttpPost("{projectId}/likes")] + public async Task LikeOrUnLikeProject([FromRoute] long projectId, [AllowedValues(Constants.PROJECT_LIKE_ACTION, Constants.PROJECT_UNLIKE_ACTION)] string action) { - if (file == null || file.Length == 0) - { - return BadRequest(new ApiResponse - { - IsSuccess = false, - Results = null, - Errors = new List { new Error { Code = "400", Message = "No file uploaded." } } - }); - } - - var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" }; - var extension = Path.GetExtension(file.FileName).ToLower(); - if (!allowedExtensions.Contains(extension)) - { - return BadRequest(new ApiResponse - { - IsSuccess = false, - Results = null, - Errors = new List { new Error { Code = "400", Message = "Invalid file type. Only image files are allowed." } } - }); - } - - if (file.Length > 5 * 1024 * 1024) - { - return BadRequest(new ApiResponse - { - IsSuccess = false, - Results = null, - Errors = new List { new Error { Code = "400", Message = "File size exceeds the 5 MB limit." } } - }); - } - - // Define the file path to save the image (e.g., in wwwroot/images) - var uploadPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images"); - if (!Directory.Exists(uploadPath)) - { - Directory.CreateDirectory(uploadPath); - } + if (!ModelState.IsValid) + return base.CustomBadRequest(); - // Generate a unique file name - var fileName = Guid.NewGuid().ToString() + extension; - var filePath = Path.Combine(uploadPath, fileName); + long authenticatedUserId = authService.GetUserId((ClaimsIdentity)HttpContext.User.Identity); + ProjectLike? storedProjectLike = await mainAppContext.ProjectLikes.FirstOrDefaultAsync(pl => pl.ProjectId == projectId && pl.UserId == authenticatedUserId); - // Save the image to the server - using (var stream = new FileStream(filePath, FileMode.Create)) + if (storedProjectLike != null) { - await file.CopyToAsync(stream); + if (action == Constants.PROJECT_LIKE_ACTION) + return Conflict(CreateErrorResponse("409", "you cannot like the same project twice")); + await projectLikeService.UnlikeProjectAsync(storedProjectLike); + return NoContent(); } - - // Save the file metadata to the database - var project = await mainAppContext.Projects.FindAsync(id); - if (project == null) + if (storedProjectLike == null && action == Constants.PROJECT_LIKE_ACTION) { - return NotFound(new ApiResponse - { - IsSuccess = false, - Results = null, - Errors = new List { new Error { Code = "404", Message = "Project not found." } } - }); + await projectLikeService.LikeProjectAsync(authenticatedUserId, projectId); + return StatusCode(StatusCodes.Status201Created, "like submitted successfully"); } - - // Save the image path or filename to the project model - project.ImagePath = $"/images/{fileName}"; - await mainAppContext.SaveChangesAsync(); - - // Return a success response with the image URL or file path - return Ok(new ApiResponse - { - IsSuccess = true, - Results = $"/images/{fileName}", - Errors = null - }); + return NoContent(); } - //[HttpGet("{id}")] - //public IActionResult GetProject(int id) - //{ - // var project = _mainAppContext.Projects - // .Include(p => p.Client) - // .FirstOrDefault(p => p.Id == id); - - // return Ok(CreateSuccessResponse(project)); + [HttpGet("{projectId}/tasks")] + public async Task GetTasksByProjectIdAsync([FromRoute] long projectId, + [AllowedValues(Constants.TASK_STATUS_TO_DO,Constants.TASK_STATUS_DONE,Constants.TASK_STATUS_IN_PROGRESS,Constants.TASK_STATUS_IN_REVIEW,ErrorMessage = $"status should be one of the values: '{Constants.TASK_STATUS_TO_DO}', '{Constants.TASK_STATUS_DONE}', '{Constants.TASK_STATUS_IN_PROGRESS}', '{Constants.TASK_STATUS_IN_REVIEW}', or empty")] + [FromQuery] string status = "") + { + if (!ModelState.IsValid) + return base.CustomBadRequest(); + + long authenticatedUserId = authService.GetUserId((ClaimsIdentity)HttpContext.User.Identity); + Project? storedProject = await mainAppContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId); + + if (storedProject == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "project not found")); + if (authenticatedUserId != storedProject.ClientId && authenticatedUserId != storedProject.FreelancerId) + return Forbid(); + + List storedTasksDTOs = await mainAppContext.Tasks.AsNoTracking() + .Where(t => t.ProjectId == projectId && t.Status.Contains(status)) + .Select(t => TaskOutputDTO.FromTask(t)) + .ToListAsync(); + return Ok(CreateSuccessResponse(storedTasksDTOs)); + } - //} } } \ No newline at end of file diff --git a/Controllers/Mobile/v1/SkillsController.cs b/Controllers/Mobile/v1/SkillsController.cs new file mode 100644 index 0000000..2b788a3 --- /dev/null +++ b/Controllers/Mobile/v1/SkillsController.cs @@ -0,0 +1,55 @@ +using AonFreelancing.Contexts; +using AonFreelancing.Models; +using AonFreelancing.Models.DTOs; +using AonFreelancing.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Security.Claims; + +namespace AonFreelancing.Controllers.Mobile.v1 +{ + [Authorize] + [Route("api/mobile/v1/[controller]")] + [ApiController] + public class SkillsController (MainAppContext mainAppContext): BaseController + { + [Authorize(Roles =Constants.USER_TYPE_FREELANCER)] + [HttpPost] + public async Task CreateSkill([FromBody] SkillInputDTO skillInputDTO) + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + long authenticatedUserId = Convert.ToInt64(identity.FindFirst(ClaimTypes.NameIdentifier).Value); + + bool isSkillExistsForFreelancer =await mainAppContext.Skills.AsNoTracking().AnyAsync(s => s.UserId == authenticatedUserId && s.Name == skillInputDTO.Name); + + if (isSkillExistsForFreelancer) + return Conflict(CreateErrorResponse("409", "you already have this skill in your profile")); + + Skill? newSkill = Skill.FromInputDTO(skillInputDTO,authenticatedUserId); + await mainAppContext.Skills.AddAsync(newSkill); + await mainAppContext.SaveChangesAsync(); + return StatusCode(StatusCodes.Status201Created); + } + + [Authorize(Roles = Constants.USER_TYPE_FREELANCER)] + [HttpDelete("{id}")] + public async Task DeleteSkill(long id) + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + long authenticatedUserId = Convert.ToInt64(identity.FindFirst(ClaimTypes.NameIdentifier).Value); + + Skill? storedSkill= mainAppContext.Skills.Where(s=>s.Id == id).FirstOrDefault(); + if (storedSkill == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "Skill not found")); + if (authenticatedUserId != storedSkill.UserId) + return Forbid(); + + mainAppContext.Skills.Remove(storedSkill); + await mainAppContext.SaveChangesAsync(); + + return NoContent(); + } + } +} diff --git a/Controllers/Mobile/v1/TasksController.cs b/Controllers/Mobile/v1/TasksController.cs index 04529ff..33fd42d 100644 --- a/Controllers/Mobile/v1/TasksController.cs +++ b/Controllers/Mobile/v1/TasksController.cs @@ -1,97 +1,66 @@ using AonFreelancing.Contexts; -using AonFreelancing.Models.DTOs; +using AonFreelancing.Controllers; using AonFreelancing.Models; +using AonFreelancing.Models.DTOs; +using AonFreelancing.Services; using AonFreelancing.Utilities; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using System; -using System.Diagnostics; - +using System.Security.Claims; -namespace AonFreelancing.Controllers.Mobile.v1 +[Authorize] +[Route("api/mobile/v1/tasks")] +[ApiController] +public class TasksController(MainAppContext mainAppContext,AuthService authService) : BaseController { - [Authorize] - [Route("api/[controller]")] - [ApiController] - public class TasksController(MainAppContext mainAppContext, UserManager userManager) : BaseController + [Authorize(Roles = $"{Constants.USER_TYPE_CLIENT}, {Constants.USER_TYPE_FREELANCER}")] + [HttpPatch("{id}/update-status")] + public async Task UpdateByIdAsync(long id, [FromBody] TaskStatusDto taskStatusDTO) { - [Authorize(Roles = "CLIENT, FREELANCER")] - [HttpPut("tasks/{id}/updateStatus")] - public async Task UpdateTaskAsync(long id, [FromBody] TaskUpdateDTO taskUpdateDTO) - { - //get task first and check its status if exist - var task = await mainAppContext.Tasks.FindAsync(id); - if (task != null && !task.IsDeleted) - { - //if its already done cant make any change to it - if (task.Status == Constants.TASKS_STATUS_DONE) - { - return BadRequest(new ApiResponse - { - IsSuccess = false, - Results = "task is already done", - }); - } - //update task status - //if new status is done we should update completedAt field too - if (taskUpdateDTO.Status == Constants.TASKS_STATUS_DONE) - task.CompletedAt = DateTime.Now; - task.Status = taskUpdateDTO.Status; - task.DeadlineAt = taskUpdateDTO.deadlineAt; - task.Name = taskUpdateDTO.Name; - task.Notes = taskUpdateDTO.notes; - await mainAppContext.SaveChangesAsync(); - return Ok(CreateSuccessResponse("Task Has Been Updated ")); - - } - return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), - "Task not found.")); - } + if (!ModelState.IsValid) + return CustomBadRequest(); + long authenticatedUserId = authService.GetUserId((ClaimsIdentity) HttpContext.User.Identity); - - - [Authorize(Roles = "CLIENT")] - [HttpPut("tasks/{pid}/checkProgress")] - public async Task CheckProgressStatusAsync( int pid ) + var storedTask = await mainAppContext.Tasks.Include(t=>t.Project) + .Where(t => t.Id == id && !t.IsDeleted) + .FirstOrDefaultAsync(); + if (storedTask == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "task not found")); + if (authenticatedUserId != storedTask.Project.ClientId && authenticatedUserId != storedTask.Project.FreelancerId) + return Forbid(); + if (taskStatusDTO.NewStatus == Constants.TASK_STATUS_DONE) { - decimal countDone= await mainAppContext.Tasks.Where(s => s.Status== Constants.TASKS_STATUS_DONE&&s.ProjectId==pid && s.IsDeleted==false).CountAsync(); - decimal countTotal = await mainAppContext.Tasks.Where(s => s.ProjectId == pid && s.IsDeleted == false ).CountAsync(); - if (countTotal > 0) - { - int progress = (int)Math.Round(countDone / countTotal * 100); - - return Ok(CreateSuccessResponse(progress)); - - } - return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), - "project has no tasks")); + if (storedTask.Status == Constants.TASK_STATUS_DONE) + return Conflict(CreateErrorResponse(StatusCodes.Status409Conflict.ToString(), "this task status is 'done' already")); + if (storedTask.Status != Constants.TASK_STATUS_DONE) + storedTask.CompletedAt = DateTime.Now; } + + storedTask.Status = taskStatusDTO.NewStatus; + await mainAppContext.SaveChangesAsync(); - [Authorize(Roles = "CLIENT, FREELANCER")] - [HttpPut("tasks/{id}/deleteTask")] - public async Task DeleteTaskAsync(long id) - { - //get task first and check its status if exist - var task = await mainAppContext.Tasks.FindAsync(id); - if (task != null) - { - - //delete task status - //if status is deleted we should update DeletedAt and IsDeleted field too - - task.IsDeleted = true; - task.DeletedAt = DateTime.Now; - await mainAppContext.SaveChangesAsync(); - return Ok(CreateSuccessResponse("Task Has Been deleted ")); - - } - return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), - "Task not found.")); + return Ok(CreateSuccessResponse(TaskOutputDTO.FromTask(storedTask))); + } + [Authorize(Roles = Constants.USER_TYPE_CLIENT)] + [HttpPut("{id}")] + public async Task UpdateByIdAsync(long id, [FromBody] TaskInputDTO taskInputDTO) + { + if (!ModelState.IsValid) + return CustomBadRequest(); + long authenticatedClientId = authService.GetUserId((ClaimsIdentity)HttpContext.User.Identity); + TaskEntity? storedTask = await mainAppContext.Tasks.Include(t=>t.Project) + .Where(t => t.Id == id && !t.IsDeleted) + .FirstOrDefaultAsync(); + if (storedTask == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "task not found")); + if (authenticatedClientId != storedTask.Project.ClientId ) + return Forbid(); + storedTask.UpdateFromInputDTO(taskInputDTO); + await mainAppContext.SaveChangesAsync(); - } + return Ok(CreateSuccessResponse(TaskOutputDTO.FromTask(storedTask))); } -} + +} \ No newline at end of file diff --git a/Controllers/Mobile/v1/UsersController.cs b/Controllers/Mobile/v1/UsersController.cs index 4c70fef..42074e0 100644 --- a/Controllers/Mobile/v1/UsersController.cs +++ b/Controllers/Mobile/v1/UsersController.cs @@ -6,6 +6,8 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using System.Collections.Immutable; +using System.Security.Claims; namespace AonFreelancing.Controllers.Mobile.v1 { @@ -16,64 +18,71 @@ public class UsersController(MainAppContext mainAppContext, RoleManager GetProfileByIdAsync([FromRoute]long id) + public async Task GetProfileByIdAsync([FromRoute] long id) { - var freelancer = await mainAppContext.Users - .OfType().Where(f => f.Id == id) - .Select(f => new FreelancerResponseDTO - { - Id = f.Id, - Name = f.Name, - Username = f.UserName ?? string.Empty, - PhoneNumber = f.PhoneNumber ?? string.Empty, - UserType = Constants.USER_TYPE_FREELANCER, - IsPhoneNumberVerified = f.PhoneNumberConfirmed, - Role = new RoleResponseDTO { Name = Constants.USER_TYPE_FREELANCER }, - Skills = f.Skills, - }).FirstOrDefaultAsync(); - + FreelancerResponseDTO? storedFreelancerDTO = await mainAppContext.Users.OfType() + .Include(f=>f.Skills) + .Where(f => f.Id == id) + .Select(f => FreelancerResponseDTO.FromFreelancer(f)) + .FirstOrDefaultAsync(); - if (freelancer != null) - return Ok(new ApiResponse - { - IsSuccess = true, - Results = freelancer, - Errors = null - }); + if (storedFreelancerDTO != null) + return Ok(CreateSuccessResponse(storedFreelancerDTO)); - var client = await mainAppContext.Users + ClientResponseDTO? storedClientDTO = await mainAppContext.Users .OfType() .Where(c => c.Id == id) .Include(c => c.Projects) .Select(c => new ClientResponseDTO - { - Id = c.Id, - Name = c.Name, - Username = c.UserName ?? string.Empty, - PhoneNumber = c.PhoneNumber ?? string.Empty, - UserType = Constants.USER_TYPE_CLIENT, - IsPhoneNumberVerified = c.PhoneNumberConfirmed, - Role = new RoleResponseDTO { Name = Constants.USER_TYPE_CLIENT }, - Projects = c.Projects.Select(p => new ProjectDetailsDTO - { - Id = p.Id, - Description = p.Description, - EndDate = p.EndDate, - StartDate = p.StartDate, - Name = p.Title, - }), - CompanyName = c.CompanyName, + { + Id = c.Id, + Name = c.Name, + Username = c.UserName ?? string.Empty, + PhoneNumber = c.PhoneNumber ?? string.Empty, + UserType = Constants.USER_TYPE_CLIENT, + IsPhoneNumberVerified = c.PhoneNumberConfirmed, + Role = new RoleResponseDTO { Name = Constants.USER_TYPE_CLIENT }, + Projects = c.Projects.Select(p => new ProjectDetailsDTO + { + Id = p.Id, + Description = p.Description, + EndDate = p.EndDate, + StartDate = p.StartDate, + Name = p.Title, + }), + CompanyName = c.CompanyName, - }).FirstOrDefaultAsync(); + }).FirstOrDefaultAsync(); - if (client != null) - return Ok(CreateSuccessResponse(client)); + if (storedClientDTO != null) + return Ok(CreateSuccessResponse(storedClientDTO)); return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "NotFound")); } + [HttpGet("/statistics")] + public async Task GetUserStatistics() + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + long authenticatedUserId = Convert.ToInt64(identity.FindFirst(ClaimTypes.NameIdentifier).Value); + //User? authenticatedUser = await userManager.GetUserAsync(HttpContext.User); + + var storedProjects = await mainAppContext.Projects.AsNoTracking() + .Include(p => p.Freelancer) + .Include(p => p.Tasks) + .Where(p => p.ClientId == authenticatedUserId || p.FreelancerId == authenticatedUserId) + .ToListAsync(); + var storedTasks = storedProjects.SelectMany(p => p.Tasks) + .ToList(); + return Ok(CreateSuccessResponse(new UserStatisticsDTO(ProjectsStatisticsDTO.FromProjects(storedProjects), + TasksStatisticsDTO.FromTasks(storedTasks) + ) + )); + + } + } - + } diff --git a/Controllers/Web/v1/AuthController.cs b/Controllers/Web/v1/AuthController.cs index 831f282..3b572f8 100644 --- a/Controllers/Web/v1/AuthController.cs +++ b/Controllers/Web/v1/AuthController.cs @@ -108,7 +108,7 @@ public async Task CompleteRegistrationAsync([FromBody] RegisterRe UserName = registerReq.Username, PhoneNumber = tempUser.PhoneNumber, PhoneNumberConfirmed = tempUser.PhoneNumberConfirmed, - Skills = registerReq.Skills ?? string.Empty, + //Skills = registerReq.Skills , }, Constants.USER_TYPE_CLIENT => new Client() { diff --git a/Controllers/Web/v1/UsersController.cs b/Controllers/Web/v1/UsersController.cs index 39804c6..aafb882 100644 --- a/Controllers/Web/v1/UsersController.cs +++ b/Controllers/Web/v1/UsersController.cs @@ -1,6 +1,7 @@ using AonFreelancing.Contexts; using AonFreelancing.Models; using AonFreelancing.Models.DTOs; +using AonFreelancing.Models.Responses; using AonFreelancing.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -18,31 +19,22 @@ public class UsersController(MainAppContext mainAppContext, RoleManager GetProfileByIdAsync([FromRoute]long id) { - var freelancer = await mainAppContext.Users - .OfType().Where(f => f.Id == id) - .Select(f => new FreelancerResponseDTO - { - Id = f.Id, - Name = f.Name, - Username = f.UserName ?? string.Empty, - PhoneNumber = f.PhoneNumber ?? string.Empty, - UserType = Constants.USER_TYPE_FREELANCER, - IsPhoneNumberVerified = f.PhoneNumberConfirmed, - Role = new RoleResponseDTO { Name = Constants.USER_TYPE_FREELANCER }, - Skills = f.Skills, - }).FirstOrDefaultAsync(); - +FreelancerResponseDTO? storedFreelancerDTO = await mainAppContext.Users.OfType() + .Include(f=>f.Skills) + .Where(f => f.Id == id) + .Select(f => FreelancerResponseDTO.FromFreelancer(f)) + .FirstOrDefaultAsync(); - if (freelancer != null) + if (storedFreelancerDTO != null) return Ok(new ApiResponse { IsSuccess = true, - Results = freelancer, + Results = storedFreelancerDTO, Errors = null }); - var client = await mainAppContext.Users + ClientResponseDTO? storedClientDTO = await mainAppContext.Users .OfType() .Where(c => c.Id == id) .Include(c => c.Projects) @@ -68,8 +60,8 @@ public async Task GetProfileByIdAsync([FromRoute]long id) }).FirstOrDefaultAsync(); - if (client != null) - return Ok(CreateSuccessResponse(client)); + if (storedClientDTO != null) + return Ok(CreateSuccessResponse(storedClientDTO)); return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "NotFound")); diff --git a/Middlewares/ExceptionHandlingMiddleware.cs b/Middlewares/ExceptionHandlingMiddleware.cs index 3eed434..69438af 100644 --- a/Middlewares/ExceptionHandlingMiddleware.cs +++ b/Middlewares/ExceptionHandlingMiddleware.cs @@ -1,4 +1,5 @@ using AonFreelancing.Models; +using AonFreelancing.Models.Responses; namespace AonFreelancing.Middlewares { diff --git a/Migrations/20241123104906_Week08Mig.Designer.cs b/Migrations/20241126110848_init-mig.Designer.cs similarity index 99% rename from Migrations/20241123104906_Week08Mig.Designer.cs rename to Migrations/20241126110848_init-mig.Designer.cs index 2bd7ce4..881cd9b 100644 --- a/Migrations/20241123104906_Week08Mig.Designer.cs +++ b/Migrations/20241126110848_init-mig.Designer.cs @@ -12,8 +12,8 @@ namespace AonFreelancing.Migrations { [DbContext(typeof(MainAppContext))] - [Migration("20241123104906_Week08Mig")] - partial class Week08Mig + [Migration("20241126110848_init-mig")] + partial class initmig { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -148,7 +148,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("datetime2"); b.Property("Description") - .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("Duration") diff --git a/Migrations/20241123015445_task07_update.cs b/Migrations/20241126110848_init-mig.cs similarity index 99% rename from Migrations/20241123015445_task07_update.cs rename to Migrations/20241126110848_init-mig.cs index 832f046..5204cd9 100644 --- a/Migrations/20241123015445_task07_update.cs +++ b/Migrations/20241126110848_init-mig.cs @@ -6,7 +6,7 @@ namespace AonFreelancing.Migrations { /// - public partial class task07_update : Migration + public partial class initmig : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -258,7 +258,7 @@ protected override void Up(MigrationBuilder migrationBuilder) Id = table.Column(type: "bigint", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), Title = table.Column(type: "nvarchar(max)", nullable: false), - Description = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: true), ClientId = table.Column(type: "bigint", nullable: false), CreatedAt = table.Column(type: "datetime2", nullable: false), StartDate = table.Column(type: "datetime2", nullable: true), diff --git a/Migrations/20241123015445_task07_update.Designer.cs b/Migrations/20241126134541_update-tasks-table.Designer.cs similarity index 98% rename from Migrations/20241123015445_task07_update.Designer.cs rename to Migrations/20241126134541_update-tasks-table.Designer.cs index 94ee986..b8933f7 100644 --- a/Migrations/20241123015445_task07_update.Designer.cs +++ b/Migrations/20241126134541_update-tasks-table.Designer.cs @@ -12,8 +12,8 @@ namespace AonFreelancing.Migrations { [DbContext(typeof(MainAppContext))] - [Migration("20241123015445_task07_update")] - partial class task07_update + [Migration("20241126134541_update-tasks-table")] + partial class updatetaskstable { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -148,7 +148,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("datetime2"); b.Property("Description") - .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("Duration") @@ -233,13 +232,18 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Status") .IsRequired() - .HasColumnType("nvarchar(max)"); + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(max)") + .HasDefaultValue("to-do"); b.HasKey("Id"); b.HasIndex("ProjectId"); - b.ToTable("Tasks", (string)null); + b.ToTable("Tasks", null, t => + { + t.HasCheckConstraint("CK_TASK_STATUS", "[Status] IN ('done', 'in-review', 'in-progress', 'to-do')"); + }); }); modelBuilder.Entity("AonFreelancing.Models.TempUser", b => diff --git a/Migrations/20241126134541_update-tasks-table.cs b/Migrations/20241126134541_update-tasks-table.cs new file mode 100644 index 0000000..ab8f297 --- /dev/null +++ b/Migrations/20241126134541_update-tasks-table.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AonFreelancing.Migrations +{ + /// + public partial class updatetaskstable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Status", + table: "Tasks", + type: "nvarchar(max)", + nullable: false, + defaultValue: "to-do", + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AddCheckConstraint( + name: "CK_TASK_STATUS", + table: "Tasks", + sql: "[Status] IN ('done', 'in-review', 'in-progress', 'to-do')"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropCheckConstraint( + name: "CK_TASK_STATUS", + table: "Tasks"); + + migrationBuilder.AlterColumn( + name: "Status", + table: "Tasks", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldDefaultValue: "to-do"); + } + } +} diff --git a/Migrations/20241126154253_skills-table-mig.Designer.cs b/Migrations/20241126154253_skills-table-mig.Designer.cs new file mode 100644 index 0000000..a25df7e --- /dev/null +++ b/Migrations/20241126154253_skills-table-mig.Designer.cs @@ -0,0 +1,687 @@ +// +using System; +using AonFreelancing.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AonFreelancing.Migrations +{ + [DbContext(typeof(MainAppContext))] + [Migration("20241126154253_skills-table-mig")] + partial class skillstablemig + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AonFreelancing.Models.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + 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("AonFreelancing.Models.Bid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ClientId") + .HasColumnType("bigint"); + + b.Property("FreelancerId") + .HasColumnType("bigint"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ProposedPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("SystemUserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("FreelancerId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SystemUserId"); + + b.ToTable("Bids"); + }); + + modelBuilder.Entity("AonFreelancing.Models.OTP", b => + { + b.Property("PhoneNumber") + .HasColumnType("nvarchar(450)"); + + b.Property("Code") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsUsed") + .HasColumnType("bit"); + + b.HasKey("PhoneNumber"); + + b.ToTable("otps", null, t => + { + t.HasCheckConstraint("CK_CODE", "LEN([Code]) = 6"); + }); + }); + + modelBuilder.Entity("AonFreelancing.Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Budget") + .HasColumnType("decimal(18,2)"); + + b.Property("ClientId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Duration") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("FreelancerId") + .HasColumnType("bigint"); + + b.Property("ImagePath") + .HasColumnType("nvarchar(max)"); + + b.Property("PriceType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("QualificationName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(max)") + .HasDefaultValue("Available"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("FreelancerId"); + + b.ToTable("Projects", null, t => + { + t.HasCheckConstraint("CK_PRICE_TYPE", "[PriceType] IN ('Fixed', 'PerHour')"); + + t.HasCheckConstraint("CK_QUALIFICATION_NAME", "[QualificationName] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')"); + + t.HasCheckConstraint("CK_STATUS", "[Status] IN ('Available', 'Closed')"); + }); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("userId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("userId"); + + b.ToTable("Skills"); + }); + + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("DeadlineAt") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(max)") + .HasDefaultValue("to-do"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Tasks", null, t => + { + t.HasCheckConstraint("CK_TASK_STATUS", "[Status] IN ('done', 'in-review', 'in-progress', 'to-do')"); + }); + }); + + modelBuilder.Entity("AonFreelancing.Models.TempUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("PhoneNumber") + .IsUnique(); + + b.ToTable("TempUser", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("About") + .HasColumnType("nvarchar(max)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(450)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("PhoneNumber") + .IsUnique() + .HasFilter("[PhoneNumber] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + + b.UseTptMappingStrategy(); + }); + + 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("bigint"); + + 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("bigint"); + + 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("bigint"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + 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("AonFreelancing.Models.Client", b => + { + b.HasBaseType("AonFreelancing.Models.User"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => + { + b.HasBaseType("AonFreelancing.Models.User"); + + b.ToTable("Freelancers", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => + { + b.HasBaseType("AonFreelancing.Models.User"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.ToTable("SystemUsers", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.Bid", b => + { + b.HasOne("AonFreelancing.Models.Client", null) + .WithMany("Bids") + .HasForeignKey("ClientId"); + + b.HasOne("AonFreelancing.Models.Freelancer", "Freelancer") + .WithMany("Bids") + .HasForeignKey("FreelancerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.Project", "Project") + .WithMany("Bids") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.SystemUser", null) + .WithMany("Bids") + .HasForeignKey("SystemUserId"); + + b.Navigation("Freelancer"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("AonFreelancing.Models.OTP", b => + { + b.HasOne("AonFreelancing.Models.TempUser", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.OTP", "PhoneNumber") + .HasPrincipalKey("AonFreelancing.Models.TempUser", "PhoneNumber") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Project", b => + { + b.HasOne("AonFreelancing.Models.Client", "Client") + .WithMany("Projects") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.Freelancer", "Freelancer") + .WithMany() + .HasForeignKey("FreelancerId"); + + b.Navigation("Client"); + + b.Navigation("Freelancer"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.HasOne("AonFreelancing.Models.Freelancer", null) + .WithMany("Skills") + .HasForeignKey("userId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => + { + b.HasOne("AonFreelancing.Models.Project", "Project") + .WithMany("Tasks") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("AonFreelancing.Models.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("AonFreelancing.Models.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Client", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.Client", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.Freelancer", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.SystemUser", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Project", b => + { + b.Navigation("Bids"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Client", b => + { + b.Navigation("Bids"); + + b.Navigation("Projects"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => + { + b.Navigation("Bids"); + + b.Navigation("Skills"); + }); + + modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => + { + b.Navigation("Bids"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20241126154253_skills-table-mig.cs b/Migrations/20241126154253_skills-table-mig.cs new file mode 100644 index 0000000..76dec8a --- /dev/null +++ b/Migrations/20241126154253_skills-table-mig.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AonFreelancing.Migrations +{ + /// + public partial class skillstablemig : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Skills", + table: "Freelancers"); + + migrationBuilder.CreateTable( + name: "Skills", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + userId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Skills", x => x.Id); + table.ForeignKey( + name: "FK_Skills_Freelancers_userId", + column: x => x.userId, + principalTable: "Freelancers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Skills_userId", + table: "Skills", + column: "userId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Skills"); + + migrationBuilder.AddColumn( + name: "Skills", + table: "Freelancers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/Migrations/20241127110303_project-likes-mig.Designer.cs b/Migrations/20241127110303_project-likes-mig.Designer.cs new file mode 100644 index 0000000..ec61654 --- /dev/null +++ b/Migrations/20241127110303_project-likes-mig.Designer.cs @@ -0,0 +1,731 @@ +// +using System; +using AonFreelancing.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AonFreelancing.Migrations +{ + [DbContext(typeof(MainAppContext))] + [Migration("20241127110303_project-likes-mig")] + partial class projectlikesmig + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AonFreelancing.Models.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + 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("AonFreelancing.Models.Bid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ClientId") + .HasColumnType("bigint"); + + b.Property("FreelancerId") + .HasColumnType("bigint"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ProposedPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("SystemUserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("FreelancerId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SystemUserId"); + + b.ToTable("Bids"); + }); + + modelBuilder.Entity("AonFreelancing.Models.OTP", b => + { + b.Property("PhoneNumber") + .HasColumnType("nvarchar(450)"); + + b.Property("Code") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsUsed") + .HasColumnType("bit"); + + b.HasKey("PhoneNumber"); + + b.ToTable("otps", null, t => + { + t.HasCheckConstraint("CK_CODE", "LEN([Code]) = 6"); + }); + }); + + modelBuilder.Entity("AonFreelancing.Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Budget") + .HasColumnType("decimal(18,2)"); + + b.Property("ClientId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Duration") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("FreelancerId") + .HasColumnType("bigint"); + + b.Property("ImagePath") + .HasColumnType("nvarchar(max)"); + + b.Property("PriceType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("QualificationName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(max)") + .HasDefaultValue("Available"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("FreelancerId"); + + b.ToTable("Projects", null, t => + { + t.HasCheckConstraint("CK_PRICE_TYPE", "[PriceType] IN ('Fixed', 'PerHour')"); + + t.HasCheckConstraint("CK_QUALIFICATION_NAME", "[QualificationName] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')"); + + t.HasCheckConstraint("CK_STATUS", "[Status] IN ('Available', 'Closed')"); + }); + }); + + modelBuilder.Entity("AonFreelancing.Models.ProjectLikes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectLikes"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Skills"); + }); + + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("DeadlineAt") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(max)") + .HasDefaultValue("to-do"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Tasks", null, t => + { + t.HasCheckConstraint("CK_TASK_STATUS", "[Status] IN ('done', 'in-review', 'in-progress', 'to-do')"); + }); + }); + + modelBuilder.Entity("AonFreelancing.Models.TempUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("PhoneNumber") + .IsUnique(); + + b.ToTable("TempUser", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("About") + .HasColumnType("nvarchar(max)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(450)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("PhoneNumber") + .IsUnique() + .HasFilter("[PhoneNumber] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + + b.UseTptMappingStrategy(); + }); + + 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("bigint"); + + 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("bigint"); + + 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("bigint"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + 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("AonFreelancing.Models.Client", b => + { + b.HasBaseType("AonFreelancing.Models.User"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => + { + b.HasBaseType("AonFreelancing.Models.User"); + + b.ToTable("Freelancers", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => + { + b.HasBaseType("AonFreelancing.Models.User"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.ToTable("SystemUsers", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.Bid", b => + { + b.HasOne("AonFreelancing.Models.Client", null) + .WithMany("Bids") + .HasForeignKey("ClientId"); + + b.HasOne("AonFreelancing.Models.Freelancer", "Freelancer") + .WithMany("Bids") + .HasForeignKey("FreelancerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.Project", "Project") + .WithMany("Bids") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.SystemUser", null) + .WithMany("Bids") + .HasForeignKey("SystemUserId"); + + b.Navigation("Freelancer"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("AonFreelancing.Models.OTP", b => + { + b.HasOne("AonFreelancing.Models.TempUser", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.OTP", "PhoneNumber") + .HasPrincipalKey("AonFreelancing.Models.TempUser", "PhoneNumber") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Project", b => + { + b.HasOne("AonFreelancing.Models.Client", "Client") + .WithMany("Projects") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.Freelancer", "Freelancer") + .WithMany() + .HasForeignKey("FreelancerId"); + + b.Navigation("Client"); + + b.Navigation("Freelancer"); + }); + + modelBuilder.Entity("AonFreelancing.Models.ProjectLikes", b => + { + b.HasOne("AonFreelancing.Models.Project", null) + .WithMany("ProjectLikes") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.HasOne("AonFreelancing.Models.Freelancer", null) + .WithMany("Skills") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => + { + b.HasOne("AonFreelancing.Models.Project", "Project") + .WithMany("Tasks") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("AonFreelancing.Models.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("AonFreelancing.Models.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Client", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.Client", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.Freelancer", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.SystemUser", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Project", b => + { + b.Navigation("Bids"); + + b.Navigation("ProjectLikes"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Client", b => + { + b.Navigation("Bids"); + + b.Navigation("Projects"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => + { + b.Navigation("Bids"); + + b.Navigation("Skills"); + }); + + modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => + { + b.Navigation("Bids"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20241127110303_project-likes-mig.cs b/Migrations/20241127110303_project-likes-mig.cs new file mode 100644 index 0000000..15466f0 --- /dev/null +++ b/Migrations/20241127110303_project-likes-mig.cs @@ -0,0 +1,103 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AonFreelancing.Migrations +{ + /// + public partial class projectlikesmig : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Skills_Freelancers_userId", + table: "Skills"); + + migrationBuilder.RenameColumn( + name: "userId", + table: "Skills", + newName: "UserId"); + + migrationBuilder.RenameIndex( + name: "IX_Skills_userId", + table: "Skills", + newName: "IX_Skills_UserId"); + + migrationBuilder.CreateTable( + name: "ProjectLikes", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ProjectId = table.Column(type: "bigint", nullable: false), + UserId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProjectLikes", x => x.Id); + table.ForeignKey( + name: "FK_ProjectLikes_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProjectLikes_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_ProjectLikes_ProjectId_UserId", + table: "ProjectLikes", + columns: new[] { "ProjectId", "UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProjectLikes_UserId", + table: "ProjectLikes", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Skills_Freelancers_UserId", + table: "Skills", + column: "UserId", + principalTable: "Freelancers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Skills_Freelancers_UserId", + table: "Skills"); + + migrationBuilder.DropTable( + name: "ProjectLikes"); + + migrationBuilder.RenameColumn( + name: "UserId", + table: "Skills", + newName: "userId"); + + migrationBuilder.RenameIndex( + name: "IX_Skills_UserId", + table: "Skills", + newName: "IX_Skills_userId"); + + migrationBuilder.AddForeignKey( + name: "FK_Skills_Freelancers_userId", + table: "Skills", + column: "userId", + principalTable: "Freelancers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Migrations/20241127185547_project-image-mig2.Designer.cs b/Migrations/20241127185547_project-image-mig2.Designer.cs new file mode 100644 index 0000000..0a98b9c --- /dev/null +++ b/Migrations/20241127185547_project-image-mig2.Designer.cs @@ -0,0 +1,731 @@ +// +using System; +using AonFreelancing.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AonFreelancing.Migrations +{ + [DbContext(typeof(MainAppContext))] + [Migration("20241127185547_project-image-mig2")] + partial class projectimagemig2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AonFreelancing.Models.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + 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("AonFreelancing.Models.Bid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ClientId") + .HasColumnType("bigint"); + + b.Property("FreelancerId") + .HasColumnType("bigint"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ProposedPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SubmittedAt") + .HasColumnType("datetime2"); + + b.Property("SystemUserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("FreelancerId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SystemUserId"); + + b.ToTable("Bids"); + }); + + modelBuilder.Entity("AonFreelancing.Models.OTP", b => + { + b.Property("PhoneNumber") + .HasColumnType("nvarchar(450)"); + + b.Property("Code") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsUsed") + .HasColumnType("bit"); + + b.HasKey("PhoneNumber"); + + b.ToTable("otps", null, t => + { + t.HasCheckConstraint("CK_CODE", "LEN([Code]) = 6"); + }); + }); + + modelBuilder.Entity("AonFreelancing.Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Budget") + .HasColumnType("decimal(18,2)"); + + b.Property("ClientId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Duration") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("FreelancerId") + .HasColumnType("bigint"); + + b.Property("ImageFileName") + .HasColumnType("nvarchar(max)"); + + b.Property("PriceType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("QualificationName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(max)") + .HasDefaultValue("Available"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("FreelancerId"); + + b.ToTable("Projects", null, t => + { + t.HasCheckConstraint("CK_PRICE_TYPE", "[PriceType] IN ('Fixed', 'PerHour')"); + + t.HasCheckConstraint("CK_QUALIFICATION_NAME", "[QualificationName] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')"); + + t.HasCheckConstraint("CK_STATUS", "[Status] IN ('Available', 'Closed')"); + }); + }); + + modelBuilder.Entity("AonFreelancing.Models.ProjectLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectLikes"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Skills"); + }); + + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("DeadlineAt") + .HasColumnType("datetime2"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(max)") + .HasDefaultValue("to-do"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Tasks", null, t => + { + t.HasCheckConstraint("CK_TASK_STATUS", "[Status] IN ('done', 'in-review', 'in-progress', 'to-do')"); + }); + }); + + modelBuilder.Entity("AonFreelancing.Models.TempUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("PhoneNumber") + .IsUnique(); + + b.ToTable("TempUser", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("About") + .HasColumnType("nvarchar(max)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(450)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("PhoneNumber") + .IsUnique() + .HasFilter("[PhoneNumber] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + + b.UseTptMappingStrategy(); + }); + + 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("bigint"); + + 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("bigint"); + + 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("bigint"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + 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("AonFreelancing.Models.Client", b => + { + b.HasBaseType("AonFreelancing.Models.User"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => + { + b.HasBaseType("AonFreelancing.Models.User"); + + b.ToTable("Freelancers", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => + { + b.HasBaseType("AonFreelancing.Models.User"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.ToTable("SystemUsers", (string)null); + }); + + modelBuilder.Entity("AonFreelancing.Models.Bid", b => + { + b.HasOne("AonFreelancing.Models.Client", null) + .WithMany("Bids") + .HasForeignKey("ClientId"); + + b.HasOne("AonFreelancing.Models.Freelancer", "Freelancer") + .WithMany("Bids") + .HasForeignKey("FreelancerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.Project", "Project") + .WithMany("Bids") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.SystemUser", null) + .WithMany("Bids") + .HasForeignKey("SystemUserId"); + + b.Navigation("Freelancer"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("AonFreelancing.Models.OTP", b => + { + b.HasOne("AonFreelancing.Models.TempUser", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.OTP", "PhoneNumber") + .HasPrincipalKey("AonFreelancing.Models.TempUser", "PhoneNumber") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Project", b => + { + b.HasOne("AonFreelancing.Models.Client", "Client") + .WithMany("Projects") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.Freelancer", "Freelancer") + .WithMany() + .HasForeignKey("FreelancerId"); + + b.Navigation("Client"); + + b.Navigation("Freelancer"); + }); + + modelBuilder.Entity("AonFreelancing.Models.ProjectLike", b => + { + b.HasOne("AonFreelancing.Models.Project", null) + .WithMany("ProjectLikes") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.HasOne("AonFreelancing.Models.Freelancer", null) + .WithMany("Skills") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => + { + b.HasOne("AonFreelancing.Models.Project", "Project") + .WithMany("Tasks") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("AonFreelancing.Models.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("AonFreelancing.Models.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Client", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.Client", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.Freelancer", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => + { + b.HasOne("AonFreelancing.Models.User", null) + .WithOne() + .HasForeignKey("AonFreelancing.Models.SystemUser", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Project", b => + { + b.Navigation("Bids"); + + b.Navigation("ProjectLikes"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Client", b => + { + b.Navigation("Bids"); + + b.Navigation("Projects"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => + { + b.Navigation("Bids"); + + b.Navigation("Skills"); + }); + + modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => + { + b.Navigation("Bids"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20241123104906_Week08Mig.cs b/Migrations/20241127185547_project-image-mig2.cs similarity index 51% rename from Migrations/20241123104906_Week08Mig.cs rename to Migrations/20241127185547_project-image-mig2.cs index 1f98dff..3f24b71 100644 --- a/Migrations/20241123104906_Week08Mig.cs +++ b/Migrations/20241127185547_project-image-mig2.cs @@ -5,18 +5,24 @@ namespace AonFreelancing.Migrations { /// - public partial class Week08Mig : Migration + public partial class projectimagemig2 : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { - + migrationBuilder.RenameColumn( + name: "ImagePath", + table: "Projects", + newName: "ImageFileName"); } /// protected override void Down(MigrationBuilder migrationBuilder) { - + migrationBuilder.RenameColumn( + name: "ImageFileName", + table: "Projects", + newName: "ImagePath"); } } } diff --git a/Migrations/MainAppContextModelSnapshot.cs b/Migrations/MainAppContextModelSnapshot.cs index 5fa3efc..d6eaee9 100644 --- a/Migrations/MainAppContextModelSnapshot.cs +++ b/Migrations/MainAppContextModelSnapshot.cs @@ -145,7 +145,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime2"); b.Property("Description") - .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("Duration") @@ -157,7 +156,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FreelancerId") .HasColumnType("bigint"); - b.Property("ImagePath") + b.Property("ImageFileName") .HasColumnType("nvarchar(max)"); b.Property("PriceType") @@ -197,6 +196,55 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("AonFreelancing.Models.ProjectLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectLikes"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Skills"); + }); + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => { b.Property("Id") @@ -230,13 +278,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Status") .IsRequired() - .HasColumnType("nvarchar(max)"); + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(max)") + .HasDefaultValue("to-do"); b.HasKey("Id"); b.HasIndex("ProjectId"); - b.ToTable("Tasks", (string)null); + b.ToTable("Tasks", null, t => + { + t.HasCheckConstraint("CK_TASK_STATUS", "[Status] IN ('done', 'in-review', 'in-progress', 'to-do')"); + }); }); modelBuilder.Entity("AonFreelancing.Models.TempUser", b => @@ -461,10 +514,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasBaseType("AonFreelancing.Models.User"); - b.Property("Skills") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.ToTable("Freelancers", (string)null); }); @@ -533,6 +582,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Freelancer"); }); + modelBuilder.Entity("AonFreelancing.Models.ProjectLike", b => + { + b.HasOne("AonFreelancing.Models.Project", null) + .WithMany("ProjectLikes") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.HasOne("AonFreelancing.Models.Freelancer", null) + .WithMany("Skills") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => { b.HasOne("AonFreelancing.Models.Project", "Project") @@ -626,6 +699,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("Bids"); + b.Navigation("ProjectLikes"); + b.Navigation("Tasks"); }); @@ -639,6 +714,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => { b.Navigation("Bids"); + + b.Navigation("Skills"); }); modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => diff --git a/Models/Bid.cs b/Models/Bid.cs index 67c4c7b..11440c7 100644 --- a/Models/Bid.cs +++ b/Models/Bid.cs @@ -1,4 +1,6 @@ -using System.ComponentModel.DataAnnotations.Schema; +using AonFreelancing.Models.DTOs; +using AonFreelancing.Utilities; +using System.ComponentModel.DataAnnotations.Schema; namespace AonFreelancing.Models { @@ -12,8 +14,22 @@ public class Bid public Freelancer Freelancer { get; set; } public decimal ProposedPrice { get; set; } public string? Notes { get; set; } - public string Status { get; set; } = "pending"; - public DateTime SubmittedAt { get; set; } = DateTime.Now; + public string Status { get; set; } + public DateTime SubmittedAt { get; set; } public DateTime? ApprovedAt { get; set; } + + public Bid() { } + + Bid(BidInputDto inputDto,long freelancerId,long projectId) + { + FreelancerId = freelancerId; + ProjectId = projectId; + ProposedPrice = inputDto.ProposedPrice; + Notes = inputDto.Notes; + Status = Constants.BIDS_STATUS_PENDING; + SubmittedAt = DateTime.Now; + } + + public static Bid FromInputDTO(BidInputDto inputDto,long freelancerId,long projectId) =>new Bid(inputDto,freelancerId,projectId); } } diff --git a/Models/DTOs/BidOutDto.cs b/Models/DTOs/BidOutDto.cs deleted file mode 100644 index 5c94326..0000000 --- a/Models/DTOs/BidOutDto.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace AonFreelancing.Models.DTOs -{ - public class BidOutDto - { - public long Id { get; set; } - public long ProjectId { get; set; } - - public ProjectOutDTO Project { get; set; } - public long FreelancerId { get; set; } - public FreelancerShortOutDTO Freelancer { get; set; } - public decimal ProposedPrice { get; set; } - public string? Notes { get; set; } - public string Status { get; set; } = "pending"; - public DateTime SubmittedAt { get; set; } = DateTime.Now; - public DateTime? ApprovedAt { get; set; } - } -} diff --git a/Models/DTOs/BidOutputDTO.cs b/Models/DTOs/BidOutputDTO.cs new file mode 100644 index 0000000..168e15b --- /dev/null +++ b/Models/DTOs/BidOutputDTO.cs @@ -0,0 +1,32 @@ +namespace AonFreelancing.Models.DTOs +{ + public class BidOutputDTO + { + public long Id { get; set; } + public long ProjectId { get; set; } + + //public ProjectOutDTO Project { get; set; } + //public long FreelancerId { get; set; } + public FreelancerShortOutDTO Freelancer { get; set; } + public decimal ProposedPrice { get; set; } + public string? Notes { get; set; } + public string Status { get; set; } = "pending"; + public DateTime SubmittedAt { get; set; } = DateTime.Now; + public DateTime? ApprovedAt { get; set; } + + BidOutputDTO(Bid bid) + { + Id = bid.Id; + //FreelancerId = bid.FreelancerId; + ProjectId = bid.ProjectId; + Freelancer = FreelancerShortOutDTO.FromFreelancer(bid.Freelancer); + ProposedPrice = bid.ProposedPrice; + Notes = bid.Notes; + Status = bid.Status; + SubmittedAt = bid.SubmittedAt; + ApprovedAt = bid.ApprovedAt; + } + public static BidOutputDTO FromBid(Bid bid) => new BidOutputDTO(bid); + + } +} diff --git a/Models/DTOs/FreelancerDTO.cs b/Models/DTOs/FreelancerDTO.cs index 016b87f..b7e8f48 100644 --- a/Models/DTOs/FreelancerDTO.cs +++ b/Models/DTOs/FreelancerDTO.cs @@ -2,27 +2,45 @@ namespace AonFreelancing.Models.DTOs { - public class FreelancerDTO:UserDTO - { + //public class FreelancerDTO : UserDTO + //{ - public string Skills { get; set; } - } + // public string Skills { get; set; } + //} - public class FreelancerRequestDTO : UserDTO - { - public string Skills { get; set; } - } + //public class FreelancerRequestDTO : UserDTO + //{ + // public string Skills { get; set; } + //} - public class FreelancerResponseDTO : UserResponseDTO { - public string? Skills { get; set; } - + public class FreelancerResponseDTO : UserResponseDTO + { + public ListSkills { get; set; } + FreelancerResponseDTO(Freelancer freelancer) + { + Id = freelancer.Id; + Name = freelancer.Name; + Username = freelancer.UserName; + PhoneNumber = freelancer.PhoneNumber; + UserType = Constants.USER_TYPE_FREELANCER; + IsPhoneNumberVerified = freelancer.PhoneNumberConfirmed; + Role = new RoleResponseDTO { Name = Constants.USER_TYPE_FREELANCER }; + Skills = freelancer.Skills.Select(s => SkillOutDTO.FromSkill(s)).ToList(); + } + public static FreelancerResponseDTO FromFreelancer(Freelancer freelancer)=>new FreelancerResponseDTO(freelancer); } public class FreelancerShortOutDTO { public long Id { get; set; } public string Name { get; set; } - public string QualificationName { get; set; } + + FreelancerShortOutDTO(Freelancer freelancer) + { + Id = freelancer.Id; + Name = freelancer.Name; + } + public static FreelancerShortOutDTO FromFreelancer(Freelancer freelancer) => new FreelancerShortOutDTO(freelancer); } } diff --git a/Models/DTOs/PriceRange.cs b/Models/DTOs/PriceRange.cs new file mode 100644 index 0000000..7bed511 --- /dev/null +++ b/Models/DTOs/PriceRange.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace AonFreelancing.Models.DTOs +{ + public class PriceRange:IValidatableObject + { + [Range(0, int.MaxValue)] + public decimal? MinPrice { get; set; } = 0; + + [Range(0, int.MaxValue)] + public decimal? MaxPrice { get; set; } = int.MaxValue; + + public IEnumerable Validate(ValidationContext validationContext) + { + if (MinPrice >= MaxPrice) + yield return new ValidationResult("MinPrice must be less than MaxPrice"); + } + + + } +} diff --git a/Models/DTOs/ProjectInputDTO.cs b/Models/DTOs/ProjectInputDTO.cs index 1234e59..7a5e6fc 100644 --- a/Models/DTOs/ProjectInputDTO.cs +++ b/Models/DTOs/ProjectInputDTO.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using AonFreelancing.Attributes; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; namespace AonFreelancing.Models.DTOs @@ -17,7 +18,7 @@ public class ProjectInputDto public string QualificationName { get; set; } [Required] - [Range(1,int.MaxValue)] + [Range(1, 365)] public int Duration { get; set; } //Number of days [Required] @@ -27,5 +28,9 @@ public class ProjectInputDto [Required] [Range(0, int.MaxValue)] public decimal Budget { get; set; } + + [MaxFileSize(1024 * 1024 * 5)] + [AllowedFileExtensions([".jpg", ".jpeg", ".png"])] + public IFormFile? ImageFile { get; set; } } } diff --git a/Models/DTOs/ProjectOutDTO.cs b/Models/DTOs/ProjectOutDTO.cs index 76e53c5..39157ca 100644 --- a/Models/DTOs/ProjectOutDTO.cs +++ b/Models/DTOs/ProjectOutDTO.cs @@ -1,6 +1,8 @@ using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; +using AonFreelancing.Utilities; +using System.Text.Json.Serialization; namespace AonFreelancing.Models.DTOs { @@ -9,14 +11,43 @@ public class ProjectOutDTO public long Id { get; set; } public int Duration { get; set; } public string Title { get; set; } - public string Description { get; set; } + public string? Description { get; set; } public string Qualifications { get; set; } public string PriceType { get; set; } public string Status { get; set; } public decimal Budget { get; set; } public DateTime CreatedAt { get; set; } - public DateTime? StartDate { get; set; } - public DateTime? EndDate { get; set; } + //public DateTime? StartDate { get; set; } + //public DateTime? EndDate { get; set; } public string? CreationTime { get; set; } + public string ClientName { get; set; } + public long ClientId { get; set; } + + public string ImageUrl { get; set; } + + [JsonPropertyName("likes")] + public long LikesCount { get; set; } + ProjectOutDTO(Project project,string imageBaseUrl) + { + Id = project.Id; + Duration = project.Duration; + Title = project.Title; + Description = project.Description; + Qualifications = project.QualificationName; + PriceType = project.PriceType; + Status = project.Status; + Budget = project.Budget; + CreatedAt = project.CreatedAt; + //StartDate = project.StartDate; + //EndDate = project.EndDate; + CreationTime = StringOperations.GetTimeAgo(CreatedAt); + ClientName = project.Client.Name; + ClientId = project.Client.Id; + LikesCount = project.ProjectLikes.Count(); + if (project.ImageFileName != null) + ImageUrl = $"{imageBaseUrl}/{project.ImageFileName}"; + } + public static ProjectOutDTO FromProject(Project project,string imageBaseUrl) => new ProjectOutDTO(project,imageBaseUrl); + } } diff --git a/Models/DTOs/ProjectsStatisticsDTO.cs b/Models/DTOs/ProjectsStatisticsDTO.cs new file mode 100644 index 0000000..1e3f81b --- /dev/null +++ b/Models/DTOs/ProjectsStatisticsDTO.cs @@ -0,0 +1,20 @@ +using AonFreelancing.Utilities; + +namespace AonFreelancing.Models.DTOs +{ + public class ProjectsStatisticsDTO + { + public int Total { get; set; } + public int Available { get; set; } + public int Closed { get; set; } + + ProjectsStatisticsDTO(Listprojects) + { + Total = projects.Count; ; + Available = projects.Where(p => p.Status == Constants.PROJECT_STATUS_AVAILABLE).Count(); + Closed = projects.Where(p => p.Status == Constants.PROJECT_STATUS_CLOSED).Count(); + } + + public static ProjectsStatisticsDTO FromProjects(List projects) => new ProjectsStatisticsDTO(projects); + } +} \ No newline at end of file diff --git a/Models/DTOs/SkillInputDTO.cs b/Models/DTOs/SkillInputDTO.cs new file mode 100644 index 0000000..42ac8f2 --- /dev/null +++ b/Models/DTOs/SkillInputDTO.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace AonFreelancing.Models.DTOs +{ + public class SkillInputDTO + { + [Required(ErrorMessage = "skill name cannot be null")] + public string Name { get; set; } + } +} diff --git a/Models/DTOs/SkillOutDTO.cs b/Models/DTOs/SkillOutDTO.cs new file mode 100644 index 0000000..3b7005f --- /dev/null +++ b/Models/DTOs/SkillOutDTO.cs @@ -0,0 +1,14 @@ +namespace AonFreelancing.Models.DTOs +{ + public class SkillOutDTO + { + public long Id { get; set; } + public string Name { get; set; } + SkillOutDTO(Skill skill) + { + Id = skill.Id; + Name = skill.Name; + } + public static SkillOutDTO FromSkill(Skill skill) => new SkillOutDTO(skill); + } +} \ No newline at end of file diff --git a/Models/DTOs/TaskInputDto.cs b/Models/DTOs/TaskInputDto.cs index 98281cb..b0a4551 100644 --- a/Models/DTOs/TaskInputDto.cs +++ b/Models/DTOs/TaskInputDto.cs @@ -1,6 +1,9 @@ -namespace AonFreelancing.Models.DTOs +using AonFreelancing.Utilities; +using System.ComponentModel.DataAnnotations; + +namespace AonFreelancing.Models.DTOs { - public class TaskInputDto + public class TaskInputDTO { public string Name { get; set; } public DateTime DeadlineAt { get; set; } @@ -9,6 +12,7 @@ public class TaskInputDto public class TaskStatusDto { + [AllowedValues(Constants.TASK_STATUS_DONE, Constants.TASK_STATUS_IN_REVIEW, Constants.TASK_STATUS_IN_PROGRESS, Constants.TASK_STATUS_TO_DO)] public string NewStatus { get; set; } } diff --git a/Models/DTOs/TaskOutputDTO.cs b/Models/DTOs/TaskOutputDTO.cs new file mode 100644 index 0000000..13ff9f3 --- /dev/null +++ b/Models/DTOs/TaskOutputDTO.cs @@ -0,0 +1,26 @@ +namespace AonFreelancing.Models.DTOs +{ + public class TaskOutputDTO + { + public long Id { get; set; } + public long ProjectId { get; set; } + public string Name { get; set; } + public string? Notes { get; set; } + public string Status { get; set; } + public DateTime? Deadline { get; set; } + public DateTime? CompletedAt { get; set; } + + public TaskOutputDTO() { } + TaskOutputDTO(TaskEntity task) + { + Id = task.Id; + ProjectId = task.ProjectId; + Name = task.Name; + Notes = task.Notes; + Status = task.Status; + Deadline = task.DeadlineAt; + CompletedAt = task.CompletedAt; + } + public static TaskOutputDTO FromTask(TaskEntity task) => new TaskOutputDTO(task); + } +} diff --git a/Models/DTOs/TasksStatisticsDTO.cs b/Models/DTOs/TasksStatisticsDTO.cs new file mode 100644 index 0000000..adae17e --- /dev/null +++ b/Models/DTOs/TasksStatisticsDTO.cs @@ -0,0 +1,31 @@ +using AonFreelancing.Utilities; + +namespace AonFreelancing.Models.DTOs +{ + public class TasksStatisticsDTO + { + + public int Total { get; set; } + public string ToDo { get; set; } + public string InReview { get; set; } + public string InProgress { get; set; } + public string Done { get; set; } + + TasksStatisticsDTO(List tasks) + { + Total = tasks.Count; + if (Total == 0) + return; + ToDo = GetPercentString(tasks.Where(t => t.Status == Constants.TASK_STATUS_TO_DO).Count()); + InReview =GetPercentString(tasks.Where(t => t.Status == Constants.TASK_STATUS_IN_REVIEW).Count()); + InProgress = GetPercentString(tasks.Where(t => t.Status == Constants.TASK_STATUS_IN_PROGRESS).Count()); + Done = GetPercentString(tasks.Where(t => t.Status == Constants.TASK_STATUS_DONE).Count()); + } + + public static TasksStatisticsDTO FromTasks(Listtasks)=>new TasksStatisticsDTO(tasks); + + string GetPercentString(decimal count) => $"{(count / Total) * 100}%"; + + + } +} \ No newline at end of file diff --git a/Models/DTOs/UserDTO.cs b/Models/DTOs/UserDTO.cs index aff9fd9..3284ec0 100644 --- a/Models/DTOs/UserDTO.cs +++ b/Models/DTOs/UserDTO.cs @@ -34,9 +34,9 @@ public class UserResponseDTO public long Id { get; set; } public string Name { get; set; } - public string Email { get; set; } + public string? Email { get; set; } public string? About { get; set; } - public string Username { get; set; } + public string? Username { get; set; } public string PhoneNumber { get; set; } public bool IsPhoneNumberVerified { get; set; } diff --git a/Models/DTOs/UserStatisticsDTO.cs b/Models/DTOs/UserStatisticsDTO.cs new file mode 100644 index 0000000..6178934 --- /dev/null +++ b/Models/DTOs/UserStatisticsDTO.cs @@ -0,0 +1,16 @@ +using System.Reflection.Metadata.Ecma335; + +namespace AonFreelancing.Models.DTOs +{ + public class UserStatisticsDTO + { + public ProjectsStatisticsDTO Projects { get; set; } + public TasksStatisticsDTO Tasks { get; set; } + + public UserStatisticsDTO(ProjectsStatisticsDTO projectsStatistics, TasksStatisticsDTO tasksStatistics) + { + Projects = projectsStatistics; + Tasks = tasksStatistics; + } + } +} diff --git a/Models/Freelancer.cs b/Models/Freelancer.cs index bf2b786..1609064 100644 --- a/Models/Freelancer.cs +++ b/Models/Freelancer.cs @@ -10,15 +10,7 @@ namespace AonFreelancing.Models [Table("Freelancers")] public class Freelancer : User { - - public string Skills { get; set; } - - - //public override void DisplayProfile() - //{ - // Console.WriteLine($"Overrided Method in Freelancer Class"); - //} - + public List Skills { get; set; } } diff --git a/Models/Project.cs b/Models/Project.cs index 4ef868a..d65e1c5 100644 --- a/Models/Project.cs +++ b/Models/Project.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using AonFreelancing.Models.DTOs; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; @@ -9,15 +10,15 @@ namespace AonFreelancing.Models public class Project { public long Id { get; set; } - [Required] public string Title { get; set; } + public string Title { get; set; } - [AllowNull] public string Description { get; set; } + public string? Description { get; set; } public long ClientId { get; set; } //FK // Belongs to a client [ForeignKey("ClientId")] - public Client Client { get; set; } + public Client? Client { get; set; } public DateTime CreatedAt { get; set; } public DateTime? StartDate { get; set; } @@ -29,11 +30,26 @@ public class Project public string Status { get; set; } public long? FreelancerId { get; set; } [ForeignKey("FreelancerId")] - public virtual Freelancer? Freelancer { get; set; } - public ICollection Bids { get; set; } = new List(); - public string? ImagePath { get; set; } + public Freelancer? Freelancer { get; set; } + public List Bids { get; set; } = new List(); + public string? ImageFileName { get; set; } - public ICollection Tasks { get; set; } = new List(); + public List? Tasks { get; set; } + public List? ProjectLikes { get; set; } + + public Project() { } + Project(ProjectInputDto inputDto,long clientId) + { + ClientId = clientId; + Title = inputDto.Title; + Description = inputDto.Description; + QualificationName = inputDto.QualificationName; + Duration = inputDto.Duration; + Budget = inputDto.Budget; + PriceType = inputDto.PriceType; + CreatedAt = DateTime.Now; + } + public static Project FromInputDTO(ProjectInputDto inputDto, long clientId) => new Project(inputDto, clientId); } } diff --git a/Models/ProjectLike.cs b/Models/ProjectLike.cs new file mode 100644 index 0000000..d8e9871 --- /dev/null +++ b/Models/ProjectLike.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace AonFreelancing.Models +{ + [Table("ProjectLikes")] + public class ProjectLike + { + public long Id { get; set; } + public long ProjectId { get; set; } + public long UserId { get; set; } + public DateTime CreatedAt { get; set; } + + public ProjectLike(long userId, long projectId) + { + UserId = userId; + ProjectId = projectId; + CreatedAt = DateTime.Now; + } + + } +} diff --git a/Models/Requests/PhoneNumberReq.cs b/Models/Requests/PhoneNumberReq.cs index 977179f..18945d6 100644 --- a/Models/Requests/PhoneNumberReq.cs +++ b/Models/Requests/PhoneNumberReq.cs @@ -2,7 +2,11 @@ namespace AonFreelancing.Models.Requests; -public record PhoneNumberReq( - [Required, StringLength(14, MinimumLength = 14)] - string PhoneNumber -); \ No newline at end of file +public class PhoneNumberReq +{ + [Required, StringLength(14, MinimumLength = 14)] + [Phone] + public string PhoneNumber { get; set; } + + +} \ No newline at end of file diff --git a/Models/Requests/RegisterRequest.cs b/Models/Requests/RegisterRequest.cs index 375f032..5ddcbc3 100644 --- a/Models/Requests/RegisterRequest.cs +++ b/Models/Requests/RegisterRequest.cs @@ -9,11 +9,10 @@ public record RegisterRequest( string Username, [Required, Phone] string PhoneNumber, - [Required, MinLength(4, ErrorMessage = "Too short password")] + [Required, MinLength(6, ErrorMessage = "Too short password")] string Password, [Required, AllowedValues("FREELANCER", "CLIENT")] string UserType, - string? Skills = null, string? CompanyName = null ); } diff --git a/Models/ApiResponse.cs b/Models/Responses/ApiResponse.cs similarity index 87% rename from Models/ApiResponse.cs rename to Models/Responses/ApiResponse.cs index dab67e5..bb7187c 100644 --- a/Models/ApiResponse.cs +++ b/Models/Responses/ApiResponse.cs @@ -1,4 +1,4 @@ -namespace AonFreelancing.Models +namespace AonFreelancing.Models.Responses { public class ApiResponse { diff --git a/Models/Responses/PaginatedResult.cs b/Models/Responses/PaginatedResult.cs new file mode 100644 index 0000000..75e21d6 --- /dev/null +++ b/Models/Responses/PaginatedResult.cs @@ -0,0 +1,14 @@ +namespace AonFreelancing.Models.Responses +{ + public class PaginatedResult + { + public long Total { get; set; } + public List Result { get; set; } + + public PaginatedResult(long total, List result) + { + Total = total; + Result = result; + } + } +} diff --git a/Models/Skill.cs b/Models/Skill.cs new file mode 100644 index 0000000..4d65a9d --- /dev/null +++ b/Models/Skill.cs @@ -0,0 +1,20 @@ +using AonFreelancing.Models.DTOs; +using System.ComponentModel.DataAnnotations.Schema; + +namespace AonFreelancing.Models +{ + [Table("Skills")] + public class Skill + { + public long Id { get; set; } + public long UserId { get; set; } + public string Name { get; set; } + public Skill() { } + Skill(SkillInputDTO inputDTO, long userId) + { + Name = inputDTO.Name; + UserId = userId; + } + public static Skill FromInputDTO(SkillInputDTO inputDTO, long userId) => new Skill(inputDTO, userId); + } +} diff --git a/Models/TaskEntity.cs b/Models/TaskEntity.cs index ece4483..8a6fefa 100644 --- a/Models/TaskEntity.cs +++ b/Models/TaskEntity.cs @@ -1,18 +1,35 @@ -namespace AonFreelancing.Models +using AonFreelancing.Models.DTOs; + +namespace AonFreelancing.Models { public class TaskEntity { public long Id { get; set; } public long ProjectId { get; set; } - public Project Project { get; set; } + public Project? Project { get; set; } public string Name { get; set; } - public string Status { get; set; } = "To-Do"; + public string Status { get; set; } public DateTime? DeadlineAt { get; set; } public DateTime? CompletedAt { get; set; } public string Notes { get; set; } public bool IsDeleted { get; set; } public DateTime? DeletedAt { get; set; } - + + public TaskEntity() { } + TaskEntity(TaskInputDTO inputDTO,long projectId) + { + ProjectId = projectId; + Name = inputDTO.Name; + DeadlineAt = inputDTO.DeadlineAt; + Notes = inputDTO.Notes; + } + public static TaskEntity FromInputDTO(TaskInputDTO inputDTO, long projectId) => new TaskEntity(inputDTO, projectId); + public void UpdateFromInputDTO(TaskInputDTO inputDTO) + { + Name = inputDTO.Name; + Notes = inputDTO.Notes; + DeadlineAt = inputDTO.DeadlineAt; + } } } diff --git a/Program.cs b/Program.cs index f51b34b..fbd0634 100644 --- a/Program.cs +++ b/Program.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using System.Text; @@ -23,19 +24,43 @@ public static void Main(string[] args) var conf = builder.Configuration; builder.Services.AddControllers(o => o.SuppressAsyncSuffixInActionNames = false); - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddDbContext(options => options.UseSqlServer(conf.GetConnectionString("Default"))); builder.Services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - builder.Configuration.AddJsonFile("appsettings.json"); - + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(options => + { + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() + { + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "Bearer" + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + }); + builder.Configuration.AddJsonFile("appsettings.json"); // JWT Authentication configuration var jwtSettings = builder.Configuration.GetSection("Jwt"); var key = Encoding.UTF8.GetBytes(jwtSettings["Key"] ?? string.Empty); @@ -60,32 +85,6 @@ public static void Main(string[] args) }); - builder.Services.AddSwaggerGen(options => - { - options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() - { - Name = "Authorization", - In = ParameterLocation.Header, - Type = SecuritySchemeType.Http, - Scheme = "Bearer" - }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - } - }, - Array.Empty() - } - }); - }); - - builder.Services.Configure(options => { options.SuppressModelStateInvalidFilter = true; @@ -103,12 +102,18 @@ public static void Main(string[] args) app.UseMiddleware(); - app.UseHttpsRedirection(); + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(FileStorageService.ROOT), + RequestPath = "/images" + }); app.UseAuthentication(); app.UseAuthorization(); + + app.MapControllers(); app.Run(); diff --git a/Services/AuthService.cs b/Services/AuthService.cs index eef7276..90c34f3 100644 --- a/Services/AuthService.cs +++ b/Services/AuthService.cs @@ -2,6 +2,8 @@ using AonFreelancing.Models; using AonFreelancing.Models.Requests; using Microsoft.EntityFrameworkCore; +using System.Security.Claims; +using System.Security.Principal; using static System.Net.WebRequestMethods; namespace AonFreelancing.Services @@ -12,6 +14,7 @@ public class AuthService public AuthService(MainAppContext mainAppContext) { _mainAppContext = mainAppContext; } + public long GetUserId(ClaimsIdentity identity) => long.Parse(identity.FindFirst(ClaimTypes.NameIdentifier).Value); public async Task IsUserExistsInTempAsync(PhoneNumberReq phoneNumberReq) { diff --git a/Services/FileStorageService.cs b/Services/FileStorageService.cs new file mode 100644 index 0000000..5b26d39 --- /dev/null +++ b/Services/FileStorageService.cs @@ -0,0 +1,17 @@ + +namespace AonFreelancing.Services +{ + public class FileStorageService + { + public static readonly string ROOT = Path.Combine(Directory.GetCurrentDirectory(), "uploads"); + + public async Task SaveAsync(IFormFile formFile) + { + string fileName = $"{Guid.NewGuid().ToString()}{Path.GetExtension(formFile.FileName)}"; + using Stream stream = File.Create(Path.Combine(ROOT, fileName)); + await formFile.CopyToAsync(stream); + return fileName; + } + + } +} \ No newline at end of file diff --git a/Services/ProjectLikeService.cs b/Services/ProjectLikeService.cs new file mode 100644 index 0000000..0d77894 --- /dev/null +++ b/Services/ProjectLikeService.cs @@ -0,0 +1,26 @@ +using AonFreelancing.Contexts; +using AonFreelancing.Models; + +namespace AonFreelancing.Services +{ + public class ProjectLikeService(MainAppContext mainAppContext) + { + + + + public async Task LikeProjectAsync(long userId,long projectId) + { + ProjectLike newProjectLike = new ProjectLike(userId, projectId); + await mainAppContext.AddAsync(newProjectLike); + await mainAppContext.SaveChangesAsync(); + } + public async Task UnlikeProjectAsync(ProjectLike projectLike) + { + mainAppContext.ProjectLikes.Remove(projectLike); + await mainAppContext.SaveChangesAsync(); + } + + + + } +} diff --git a/Utilities/Constants.cs b/Utilities/Constants.cs index 45ab3f7..13381f3 100644 --- a/Utilities/Constants.cs +++ b/Utilities/Constants.cs @@ -19,9 +19,12 @@ public class Constants public const string BIDS_STATUS_APPROVED = "approved"; - public const string TASKS_STATUS_TO_DO = "to-do"; - public const string TASKS_STATUS_IN_PROGRESS = "in-progress"; - public const string TASKS_STATUS_IN_REVIEW = "in-review"; - public const string TASKS_STATUS_DONE = "done"; + public const string TASK_STATUS_TO_DO = "to-do"; + public const string TASK_STATUS_IN_PROGRESS = "in-progress"; + public const string TASK_STATUS_IN_REVIEW = "in-review"; + public const string TASK_STATUS_DONE = "done"; + + public const string PROJECT_LIKE_ACTION = "like"; + public const string PROJECT_UNLIKE_ACTION = "unlike"; } } diff --git a/Utilities/FileCheckUtil.cs b/Utilities/FileCheckUtil.cs new file mode 100644 index 0000000..f1fba56 --- /dev/null +++ b/Utilities/FileCheckUtil.cs @@ -0,0 +1,44 @@ + +namespace AonFreelancing.Utilities +{ + public static class FileCheckUtil + { + // For more file signatures, see the File Signatures Database (https://www.filesignatures.net/) + private static readonly Dictionary> _fileSignature = new Dictionary> + { + { ".gif", new List { new byte[] { 0x47, 0x49, 0x46, 0x38 } } }, + { ".png", new List { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } }, + { ".jpeg", new List + { + new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, + new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 }, + new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 }, + } + }, + { ".jpg", new List + { + new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, + new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 }, + new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 }, + } + }, + }; + + public static bool IsValidFileExtensionAndSignature(string fileName, Stream data, string[] permittedExtensions) + { + if (string.IsNullOrEmpty(fileName) || data == null || data.Length == 0) + return false; + + var extension = Path.GetExtension(fileName).ToLowerInvariant(); + if (string.IsNullOrEmpty(extension) || !permittedExtensions.Contains(extension)) + return false; + + data.Position = 0; + using var reader = new BinaryReader(data); + var signatures = _fileSignature[extension]; + var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length)); + + return signatures.Any(signature => headerBytes.Take(signature.Length).SequenceEqual(signature)); + } + } +} \ No newline at end of file diff --git a/uploads/5ab065a9-5dc7-4869-be46-50429affc263.jpg b/uploads/5ab065a9-5dc7-4869-be46-50429affc263.jpg new file mode 100644 index 0000000..1a6abdc Binary files /dev/null and b/uploads/5ab065a9-5dc7-4869-be46-50429affc263.jpg differ diff --git a/wwwroot/images/f373959e-12c7-4aef-aecb-278313dbb326.jpg b/wwwroot/images/f373959e-12c7-4aef-aecb-278313dbb326.jpg deleted file mode 100644 index 63fac4e..0000000 Binary files a/wwwroot/images/f373959e-12c7-4aef-aecb-278313dbb326.jpg and /dev/null differ