feature/portfolio-admin-auth #2
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 17.9.34701.34
|
VisualStudioVersion = 18.1.11304.174
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PortBlog.API", "PortBlog.API\PortBlog.API.csproj", "{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PortBlog.API", "PortBlog.API\PortBlog.API.csproj", "{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}"
|
||||||
EndProject
|
EndProject
|
||||||
@ -15,32 +15,90 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KBR.Shared", "Shared\KBR.Sh
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KBR.Shared.Lite", "Shared\KBR.Share.Lite\KBR.Shared.Lite.csproj", "{F4B7078B-C59A-46B8-881A-C3CEE2634498}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KBR.Shared.Lite", "Shared\KBR.Share.Lite\KBR.Shared.Lite.csproj", "{F4B7078B-C59A-46B8-881A-C3CEE2634498}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PortBlog.Tests", "PortBlog.Tests\PortBlog.Tests.csproj", "{11106F82-FC17-497D-8747-CF1A84BFA7F8}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Release|Any CPU.Build.0 = Release|Any CPU
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{26654BFD-EE9B-49BA-84BA-9156AC348076}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Release|Any CPU.Build.0 = Release|Any CPU
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{DA0B3995-A83A-4D7C-A964-ADFE1F58B1FA}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{1C0F86E4-A791-4150-B9B3-B761925681DD}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{F4B7078B-C59A-46B8-881A-C3CEE2634498}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{11106F82-FC17-497D-8747-CF1A84BFA7F8}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@ -1,150 +1,420 @@
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using AutoMapper;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using PortBlog.API.Models;
|
using PortBlog.API.Models;
|
||||||
using PortBlog.API.Repositories.Contracts;
|
using PortBlog.API.Services.Contracts;
|
||||||
|
|
||||||
namespace PortBlog.API.Controllers
|
namespace PortBlog.API.Controllers
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controller for administrative actions related to candidates and their resumes.
|
/// Controller for administrative actions related to the logged-in candidate and their resume.
|
||||||
|
/// All endpoints derive the candidate identity from the access token claims.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("api/v{version:apiVersion}/admin")]
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[ApiVersion(1)]
|
[ApiVersion("1.0")]
|
||||||
|
[Route("api/v{version:apiVersion}/admin")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class AdminController(ILogger<CvController> logger, ICandidateRepository candidateRepository, IResumeRepository resumeRepository, IMapper mapper) : Controller
|
public class AdminController(ILogger<CvController> logger, IAdminService adminService) : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get hobbies of the candidate by candidateid
|
/// Get hobbies of the logged-in candidate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="candidateId">The id of the candidate whose hobbies to get</param>
|
/// <returns>Hobbies and about details of the candidate</returns>
|
||||||
/// <returns>Hobbies of the candidate</returns>
|
|
||||||
/// <response code="200">Returns the requested hobbies of the candidate</response>
|
/// <response code="200">Returns the requested hobbies of the candidate</response>
|
||||||
[HttpGet("GetHobbies/{candidateId}")]
|
[HttpGet("GetHobbies")]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<AboutDto>> GetHobbies(int candidateId)
|
public async Task<ActionResult<AboutDto>> GetHobbies()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
{
|
var aboutDetails = await adminService.GetHobbiesAsync(candidateId);
|
||||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching about details.", candidateId);
|
return Ok(aboutDetails);
|
||||||
return NotFound();
|
|
||||||
}
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
var aboutDetails = await resumeRepository.GetHobbiesAsync(candidateId);
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when fetching hobbies.");
|
||||||
return Ok(mapper.Map<AboutDto>(aboutDetails));
|
return Unauthorized(ex.Message);
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogCritical(ex, "Exception while getting about details for the candidate with id {CandidateId}.", candidateId);
|
logger.LogCritical(ex, "Exception while getting about details.");
|
||||||
return StatusCode(500, "A problem happened while handling your request.");
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get Candidate details with social links by candidateid
|
/// Get contact details (candidate with social links) for the logged-in candidate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="candidateId">The id of the candidate whose detials to get with social links</param>
|
|
||||||
/// <returns>Candidate details with social links</returns>
|
/// <returns>Candidate details with social links</returns>
|
||||||
/// <response code="200">Returns the requested candidate details with social links</response>
|
/// <response code="200">Returns the requested candidate details with social links</response>
|
||||||
[HttpGet("GetCandidateWithSocialLinks/{candidateId}")]
|
[HttpGet("GetCandidateWithSocialLinks")]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<CandidateSocialLinksDto>> GetContact(int candidateId)
|
public async Task<ActionResult<CandidateSocialLinksDto>> GetContact()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
{
|
var contact = await adminService.GetContactAsync(candidateId);
|
||||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching candidate with social links.", candidateId);
|
return Ok(contact);
|
||||||
return NotFound();
|
|
||||||
}
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
var contact = await resumeRepository.GetCandidateWithSocialLinksAsync(candidateId);
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when fetching contact.");
|
||||||
return Ok(mapper.Map<CandidateSocialLinksDto>(contact));
|
return Unauthorized(ex.Message);
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogCritical(ex, "Exception while getting contact for the candidate with social links with id {CandidateId}.", candidateId);
|
logger.LogCritical(ex, "Exception while getting contact details.");
|
||||||
return StatusCode(500, "A problem happened while handling your request.");
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get Candidate resume by candidateid
|
/// Get resume for the logged-in candidate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="candidateId">The id of the candidate whose resume to get</param>
|
|
||||||
/// <returns>Candidate resume</returns>
|
/// <returns>Candidate resume</returns>
|
||||||
/// <response code="200">Returns the requested candidate resume</response>
|
/// <response code="200">Returns the requested candidate resume</response>
|
||||||
[HttpGet("GetResume/{candidateId}")]
|
[HttpGet("GetResume")]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<ResumeDto>> GetResume(int candidateId)
|
public async Task<ActionResult<ResumeDto>> GetResume()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
{
|
var resume = await adminService.GetResumeAsync(candidateId);
|
||||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching resume.", candidateId);
|
return Ok(resume);
|
||||||
return NotFound();
|
|
||||||
}
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
var resume = await resumeRepository.GetResumeAsync(candidateId);
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when fetching resume.");
|
||||||
return Ok(mapper.Map<ResumeDto>(resume));
|
return Unauthorized(ex.Message);
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogCritical(ex, "Exception while getting resume for the candidate with id {CandidateId}.", candidateId);
|
logger.LogCritical(ex, "Exception while getting resume.");
|
||||||
return StatusCode(500, "A problem happened while handling your request.");
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get Candidate projects by candidateid
|
/// Get projects for the logged-in candidate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="candidateId">The id of the candidate whose projects to get</param>
|
|
||||||
/// <returns>Candidate projects</returns>
|
/// <returns>Candidate projects</returns>
|
||||||
/// <response code="200">Returns the requested candidate projects</response>
|
/// <response code="200">Returns the requested candidate projects</response>
|
||||||
[HttpGet("GetProjects/{candidateId}")]
|
[HttpGet("GetProjects")]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<ProjectsDto>> GetProjects(int candidateId)
|
public async Task<ActionResult<ProjectsDto>> GetProjects()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
{
|
var projects = await adminService.GetProjectsAsync(candidateId);
|
||||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching projects.", candidateId);
|
return Ok(projects);
|
||||||
return NotFound();
|
|
||||||
}
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
var projects = await resumeRepository.GetProjectsAsync(candidateId);
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when fetching projects.");
|
||||||
return Ok(mapper.Map<ProjectsDto>(projects));
|
return Unauthorized(ex.Message);
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogCritical(ex, "Exception while getting projects for the candidate with id {CandidateId}.", candidateId);
|
logger.LogCritical(ex, "Exception while getting projects.");
|
||||||
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create or update a project for the logged-in candidate's resume.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("UpsertProject")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<ProjectDto>> UpsertProject([FromBody] ProjectDto projectDto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
|
var result = await adminService.UpsertProjectAsync(candidateId, projectDto);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when upserting project.");
|
||||||
|
return Unauthorized(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogCritical(ex, "Exception while upserting project.");
|
||||||
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a project from the logged-in candidate's resume.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="projectId">The id of the project to delete</param>
|
||||||
|
[HttpDelete("DeleteProject/{projectId}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult> DeleteProject(int projectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
|
var deleted = await adminService.DeleteProjectAsync(candidateId, projectId);
|
||||||
|
if (!deleted)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when deleting project.");
|
||||||
|
return Unauthorized(ex.Message);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Resume not found when deleting project.");
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogCritical(ex, "Exception while deleting project {ProjectId}.", projectId);
|
||||||
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create, update, or remove hobbies and update the about section for the logged-in candidate.
|
||||||
|
/// Hobbies present in the list are added or updated; hobbies not in the list are removed.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("UpsertHobbies")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<AboutDto>> UpsertHobbies([FromBody] AboutDto aboutDto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
|
var result = await adminService.UpsertHobbiesAsync(candidateId, aboutDto);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when upserting hobbies.");
|
||||||
|
return Unauthorized(ex.Message);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Resume not found when upserting hobbies.");
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogCritical(ex, "Exception while upserting hobbies.");
|
||||||
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create, update, or remove skills for the logged-in candidate's resume.
|
||||||
|
/// Skills present in the list are added or updated; skills not in the list are removed.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("UpsertSkills")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IEnumerable<SkillDto>>> UpsertSkills([FromBody] IEnumerable<SkillDto> skillDtos)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
|
var result = await adminService.UpsertSkillsAsync(candidateId, skillDtos);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when upserting skills.");
|
||||||
|
return Unauthorized(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogCritical(ex, "Exception while upserting skills.");
|
||||||
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create, update, or remove academics for the logged-in candidate's resume.
|
||||||
|
/// Academics present in the list are added or updated; academics not in the list are removed.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("UpsertAcademics")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IEnumerable<AcademicDto>>> UpsertAcademics([FromBody] IEnumerable<AcademicDto> academicDtos)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
|
var result = await adminService.UpsertAcademicsAsync(candidateId, academicDtos);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when upserting academics.");
|
||||||
|
return Unauthorized(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogCritical(ex, "Exception while upserting academics.");
|
||||||
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create, update, or remove experiences for the logged-in candidate's resume.
|
||||||
|
/// Experiences present in the list are added or updated; experiences not in the list are removed.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("UpsertExperiences")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IEnumerable<ExperienceDto>>> UpsertExperiences([FromBody] IEnumerable<ExperienceDto> experienceDtos)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
|
var result = await adminService.UpsertExperiencesAsync(candidateId, experienceDtos);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when upserting experiences.");
|
||||||
|
return Unauthorized(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogCritical(ex, "Exception while upserting experiences.");
|
||||||
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create, update, or remove certifications for the logged-in candidate's resume.
|
||||||
|
/// Certifications present in the list are added or updated; certifications not in the list are removed.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("UpsertCertifications")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IEnumerable<CertificationDto>>> UpsertCertifications([FromBody] IEnumerable<CertificationDto> certificationDtos)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
|
var result = await adminService.UpsertCertificationsAsync(candidateId, certificationDtos);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when upserting certifications.");
|
||||||
|
return Unauthorized(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogCritical(ex, "Exception while upserting certifications.");
|
||||||
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create or update contact information (candidate with social links) for the logged-in candidate.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("UpsertContact")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<CandidateSocialLinksDto>> UpsertContact([FromBody] CandidateSocialLinksDto contactDto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||||
|
var result = await adminService.UpsertContactAsync(candidateId, contactDto);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Unauthorized access when upserting contact.");
|
||||||
|
return Unauthorized(ex.Message);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException ex)
|
||||||
|
{
|
||||||
|
logger.LogInformation(ex, "Resume not found when upserting contact.");
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogCritical(ex, "Exception while upserting contact.");
|
||||||
return StatusCode(500, "A problem happened while handling your request.");
|
return StatusCode(500, "A problem happened while handling your request.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,9 +12,9 @@ namespace PortBlog.API.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controller for handling authentication-related operations.
|
/// Controller for handling authentication-related operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("api/v{version:apiVersion}/auth")]
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[ApiVersion(1)]
|
[ApiVersion("1.0")]
|
||||||
|
[Route("api/v{version:apiVersion}/auth")]
|
||||||
public class AuthController(IAuthService authService, IMailService mailService, ICandidateRepository candidateRepository, ILogger<AuthController> logger, IMapper mapper, IConfiguration configuration, ITemplateService templateService, IAppDistributedCache cache) : ControllerBase
|
public class AuthController(IAuthService authService, IMailService mailService, ICandidateRepository candidateRepository, ILogger<AuthController> logger, IMapper mapper, IConfiguration configuration, ITemplateService templateService, IAppDistributedCache cache) : ControllerBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ namespace PortBlog.API.Controllers
|
|||||||
{
|
{
|
||||||
[Route("blog/api/v{version:apiVersion}/posts")]
|
[Route("blog/api/v{version:apiVersion}/posts")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[ApiVersion(1)]
|
[ApiVersion("1.0")]
|
||||||
public class BlogController : ControllerBase
|
public class BlogController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ILogger<BlogController> _logger;
|
private readonly ILogger<BlogController> _logger;
|
||||||
|
|||||||
@ -7,9 +7,9 @@ using PortBlog.API.Services.Contracts;
|
|||||||
|
|
||||||
namespace PortBlog.API.Controllers
|
namespace PortBlog.API.Controllers
|
||||||
{
|
{
|
||||||
[Route("api/v{versions:apiVersion}/cv")]
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[ApiVersion(1)]
|
[ApiVersion("1.0")]
|
||||||
|
[Route("api/v{version:apiVersion}/cv")]
|
||||||
public class CvController : ControllerBase
|
public class CvController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ILogger<CvController> _logger;
|
private readonly ILogger<CvController> _logger;
|
||||||
@ -37,7 +37,7 @@ namespace PortBlog.API.Controllers
|
|||||||
/// <response code="200">Returns the requested cv of the candidate</response>
|
/// <response code="200">Returns the requested cv of the candidate</response>
|
||||||
[HttpGet("{candidateId}")]
|
[HttpGet("{candidateId}")]
|
||||||
[Obsolete]
|
[Obsolete]
|
||||||
[ApiVersion(0.1, Deprecated = true)]
|
[ApiVersion("0.1", Deprecated = true)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Asp.Versioning;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace PortBlog.API.Controllers
|
namespace PortBlog.API.Controllers
|
||||||
{
|
{
|
||||||
[Route("api/blog/post")]
|
[ApiVersionNeutral]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Route("api/v{version:apiVersion}/blog/post")]
|
||||||
public class PostController : ControllerBase
|
public class PostController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ILogger<PostController> _logger;
|
private readonly ILogger<PostController> _logger;
|
||||||
|
|||||||
28
PortBlog.API/Extensions/ConfigureSwaggerOptions.cs
Normal file
28
PortBlog.API/Extensions/ConfigureSwaggerOptions.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Asp.Versioning.ApiExplorer;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
|
||||||
|
{
|
||||||
|
private readonly IApiVersionDescriptionProvider _provider;
|
||||||
|
|
||||||
|
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
|
||||||
|
{
|
||||||
|
_provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(SwaggerGenOptions options)
|
||||||
|
{
|
||||||
|
foreach (var description in _provider.ApiVersionDescriptions)
|
||||||
|
{
|
||||||
|
options.SwaggerDoc(
|
||||||
|
description.GroupName,
|
||||||
|
new OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = "PortBlog API",
|
||||||
|
Version = description.GroupName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,6 +21,7 @@ namespace PortBlog.API.Extensions
|
|||||||
services.AddTransient<IMailService, MailService>();
|
services.AddTransient<IMailService, MailService>();
|
||||||
services.AddTransient<IAuthService, AuthService>();
|
services.AddTransient<IAuthService, AuthService>();
|
||||||
services.AddTransient<ITemplateService, TemplateService>();
|
services.AddTransient<ITemplateService, TemplateService>();
|
||||||
|
services.AddScoped<IAdminService, AdminService>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
public class AcademicDto
|
public class AcademicDto
|
||||||
{
|
{
|
||||||
public int AcademicId { get; set; }
|
public int? AcademicId { get; set; }
|
||||||
|
|
||||||
public string Institution { get; set; } = string.Empty;
|
public string Institution { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
public class CertificationDto
|
public class CertificationDto
|
||||||
{
|
{
|
||||||
public int CertificationId { get; set; }
|
public int? CertificationId { get; set; }
|
||||||
|
|
||||||
public string CertificationName { get; set; } = string.Empty;
|
public string CertificationName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
public class ExperienceDetailsDto
|
public class ExperienceDetailsDto
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int? Id { get; set; }
|
||||||
|
|
||||||
public string Details { get; set; } = string.Empty;
|
public string Details { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ namespace PortBlog.API.Models
|
|||||||
{
|
{
|
||||||
public class ExperienceDto
|
public class ExperienceDto
|
||||||
{
|
{
|
||||||
public int ExperienceId { get; set; }
|
public int? ExperienceId { get; set; }
|
||||||
|
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
@ -12,10 +12,31 @@ namespace PortBlog.API.Models
|
|||||||
|
|
||||||
public string Company { get; set; } = string.Empty;
|
public string Company { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? Location { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full start date for write operations (e.g. "2020-01-01").
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full end date for write operations. Null means "Present".
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only display value: start year (e.g. "2020"). Mapped from entity on read.
|
||||||
|
/// </summary>
|
||||||
public string StartYear { get; set; } = string.Empty;
|
public string StartYear { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only display value: end year (e.g. "2023" or "Present"). Mapped from entity on read.
|
||||||
|
/// </summary>
|
||||||
public string EndYear { get; set; } = string.Empty;
|
public string EndYear { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only display value: formatted period (e.g. "Jan 2020 - Mar 2023"). Mapped from entity on read.
|
||||||
|
/// </summary>
|
||||||
public string Period { get; set; } = string.Empty;
|
public string Period { get; set; } = string.Empty;
|
||||||
|
|
||||||
public ICollection<ExperienceDetailsDto> Details { get; set; } = new List<ExperienceDetailsDto>();
|
public ICollection<ExperienceDetailsDto> Details { get; set; } = new List<ExperienceDetailsDto>();
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
public class HobbyDto
|
public class HobbyDto
|
||||||
{
|
{
|
||||||
public int HobbyId { get; set; }
|
public int? HobbyId { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ namespace PortBlog.API.Models
|
|||||||
{
|
{
|
||||||
public class ProjectDto
|
public class ProjectDto
|
||||||
{
|
{
|
||||||
public int ProjectId { get; set; }
|
public int? ProjectId { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
public class SkillDto
|
public class SkillDto
|
||||||
{
|
{
|
||||||
public int SkillId { get; set; }
|
public int? SkillId { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.1" />
|
||||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||||
<PackageReference Include="AutoMapper" Version="15.0.1" />
|
<PackageReference Include="AutoMapper" Version="15.0.1" />
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
|
|||||||
@ -6,46 +6,90 @@
|
|||||||
<members>
|
<members>
|
||||||
<member name="T:PortBlog.API.Controllers.AdminController">
|
<member name="T:PortBlog.API.Controllers.AdminController">
|
||||||
<summary>
|
<summary>
|
||||||
Controller for administrative actions related to candidates and their resumes.
|
Controller for administrative actions related to the logged-in candidate and their resume.
|
||||||
|
All endpoints derive the candidate identity from the access token claims.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:PortBlog.API.Controllers.AdminController.#ctor(Microsoft.Extensions.Logging.ILogger{PortBlog.API.Controllers.CvController},PortBlog.API.Repositories.Contracts.ICandidateRepository,PortBlog.API.Repositories.Contracts.IResumeRepository,AutoMapper.IMapper)">
|
<member name="M:PortBlog.API.Controllers.AdminController.#ctor(Microsoft.Extensions.Logging.ILogger{PortBlog.API.Controllers.CvController},PortBlog.API.Services.Contracts.IAdminService)">
|
||||||
<summary>
|
<summary>
|
||||||
Controller for administrative actions related to candidates and their resumes.
|
Controller for administrative actions related to the logged-in candidate and their resume.
|
||||||
|
All endpoints derive the candidate identity from the access token claims.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:PortBlog.API.Controllers.AdminController.GetHobbies(System.Int32)">
|
<member name="M:PortBlog.API.Controllers.AdminController.GetHobbies">
|
||||||
<summary>
|
<summary>
|
||||||
Get hobbies of the candidate by candidateid
|
Get hobbies of the logged-in candidate.
|
||||||
</summary>
|
</summary>
|
||||||
<param name="candidateId">The id of the candidate whose hobbies to get</param>
|
<returns>Hobbies and about details of the candidate</returns>
|
||||||
<returns>Hobbies of the candidate</returns>
|
|
||||||
<response code="200">Returns the requested hobbies of the candidate</response>
|
<response code="200">Returns the requested hobbies of the candidate</response>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:PortBlog.API.Controllers.AdminController.GetContact(System.Int32)">
|
<member name="M:PortBlog.API.Controllers.AdminController.GetContact">
|
||||||
<summary>
|
<summary>
|
||||||
Get Candidate details with social links by candidateid
|
Get contact details (candidate with social links) for the logged-in candidate.
|
||||||
</summary>
|
</summary>
|
||||||
<param name="candidateId">The id of the candidate whose detials to get with social links</param>
|
|
||||||
<returns>Candidate details with social links</returns>
|
<returns>Candidate details with social links</returns>
|
||||||
<response code="200">Returns the requested candidate details with social links</response>
|
<response code="200">Returns the requested candidate details with social links</response>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:PortBlog.API.Controllers.AdminController.GetResume(System.Int32)">
|
<member name="M:PortBlog.API.Controllers.AdminController.GetResume">
|
||||||
<summary>
|
<summary>
|
||||||
Get Candidate resume by candidateid
|
Get resume for the logged-in candidate.
|
||||||
</summary>
|
</summary>
|
||||||
<param name="candidateId">The id of the candidate whose resume to get</param>
|
|
||||||
<returns>Candidate resume</returns>
|
<returns>Candidate resume</returns>
|
||||||
<response code="200">Returns the requested candidate resume</response>
|
<response code="200">Returns the requested candidate resume</response>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:PortBlog.API.Controllers.AdminController.GetProjects(System.Int32)">
|
<member name="M:PortBlog.API.Controllers.AdminController.GetProjects">
|
||||||
<summary>
|
<summary>
|
||||||
Get Candidate projects by candidateid
|
Get projects for the logged-in candidate.
|
||||||
</summary>
|
</summary>
|
||||||
<param name="candidateId">The id of the candidate whose projects to get</param>
|
|
||||||
<returns>Candidate projects</returns>
|
<returns>Candidate projects</returns>
|
||||||
<response code="200">Returns the requested candidate projects</response>
|
<response code="200">Returns the requested candidate projects</response>
|
||||||
</member>
|
</member>
|
||||||
|
<member name="M:PortBlog.API.Controllers.AdminController.UpsertProject(PortBlog.API.Models.ProjectDto)">
|
||||||
|
<summary>
|
||||||
|
Create or update a project for the logged-in candidate's resume.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:PortBlog.API.Controllers.AdminController.DeleteProject(System.Int32)">
|
||||||
|
<summary>
|
||||||
|
Delete a project from the logged-in candidate's resume.
|
||||||
|
</summary>
|
||||||
|
<param name="projectId">The id of the project to delete</param>
|
||||||
|
</member>
|
||||||
|
<member name="M:PortBlog.API.Controllers.AdminController.UpsertHobbies(PortBlog.API.Models.AboutDto)">
|
||||||
|
<summary>
|
||||||
|
Create, update, or remove hobbies and update the about section for the logged-in candidate.
|
||||||
|
Hobbies present in the list are added or updated; hobbies not in the list are removed.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:PortBlog.API.Controllers.AdminController.UpsertSkills(System.Collections.Generic.IEnumerable{PortBlog.API.Models.SkillDto})">
|
||||||
|
<summary>
|
||||||
|
Create, update, or remove skills for the logged-in candidate's resume.
|
||||||
|
Skills present in the list are added or updated; skills not in the list are removed.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:PortBlog.API.Controllers.AdminController.UpsertAcademics(System.Collections.Generic.IEnumerable{PortBlog.API.Models.AcademicDto})">
|
||||||
|
<summary>
|
||||||
|
Create, update, or remove academics for the logged-in candidate's resume.
|
||||||
|
Academics present in the list are added or updated; academics not in the list are removed.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:PortBlog.API.Controllers.AdminController.UpsertExperiences(System.Collections.Generic.IEnumerable{PortBlog.API.Models.ExperienceDto})">
|
||||||
|
<summary>
|
||||||
|
Create, update, or remove experiences for the logged-in candidate's resume.
|
||||||
|
Experiences present in the list are added or updated; experiences not in the list are removed.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:PortBlog.API.Controllers.AdminController.UpsertCertifications(System.Collections.Generic.IEnumerable{PortBlog.API.Models.CertificationDto})">
|
||||||
|
<summary>
|
||||||
|
Create, update, or remove certifications for the logged-in candidate's resume.
|
||||||
|
Certifications present in the list are added or updated; certifications not in the list are removed.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:PortBlog.API.Controllers.AdminController.UpsertContact(PortBlog.API.Models.CandidateSocialLinksDto)">
|
||||||
|
<summary>
|
||||||
|
Create or update contact information (candidate with social links) for the logged-in candidate.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
<member name="T:PortBlog.API.Controllers.AuthController">
|
<member name="T:PortBlog.API.Controllers.AuthController">
|
||||||
<summary>
|
<summary>
|
||||||
Controller for handling authentication-related operations.
|
Controller for handling authentication-related operations.
|
||||||
@ -401,6 +445,31 @@
|
|||||||
The project categories of all the projects
|
The project categories of all the projects
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
|
<member name="P:PortBlog.API.Models.ExperienceDto.StartDate">
|
||||||
|
<summary>
|
||||||
|
Full start date for write operations (e.g. "2020-01-01").
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:PortBlog.API.Models.ExperienceDto.EndDate">
|
||||||
|
<summary>
|
||||||
|
Full end date for write operations. Null means "Present".
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:PortBlog.API.Models.ExperienceDto.StartYear">
|
||||||
|
<summary>
|
||||||
|
Read-only display value: start year (e.g. "2020"). Mapped from entity on read.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:PortBlog.API.Models.ExperienceDto.EndYear">
|
||||||
|
<summary>
|
||||||
|
Read-only display value: end year (e.g. "2023" or "Present"). Mapped from entity on read.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:PortBlog.API.Models.ExperienceDto.Period">
|
||||||
|
<summary>
|
||||||
|
Read-only display value: formatted period (e.g. "Jan 2020 - Mar 2023"). Mapped from entity on read.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
<member name="T:PortBlog.API.Models.ResumeDto">
|
<member name="T:PortBlog.API.Models.ResumeDto">
|
||||||
<summary>
|
<summary>
|
||||||
CV details of the candidate
|
CV details of the candidate
|
||||||
@ -421,6 +490,29 @@
|
|||||||
The work experiences of the candidate
|
The work experiences of the candidate
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
|
<member name="T:PortBlog.API.Repositories.ResumeRepository">
|
||||||
|
<summary>
|
||||||
|
Repository for managing resume-related data access and operations.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:PortBlog.API.Repositories.ResumeRepository.GetLatestResumeForCandidateAsync(System.Int32,System.Boolean)">
|
||||||
|
<summary>
|
||||||
|
Retrieves the latest resume for a candidate, optionally including related data.
|
||||||
|
</summary>
|
||||||
|
<param name="candidateId">The ID of the candidate.</param>
|
||||||
|
<param name="includeOtherData">If true, includes related data such as academics, experiences, social links, etc.</param>
|
||||||
|
<returns>The latest <see cref="T:PortBlog.API.Entities.Resume"/> for the candidate, or null if not found.</returns>
|
||||||
|
</member>
|
||||||
|
<member name="T:PortBlog.API.Services.AdminService">
|
||||||
|
<summary>
|
||||||
|
Service for administrative operations related to candidates, resumes, and their associated data.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:PortBlog.API.Services.AdminService.#ctor(PortBlog.API.Repositories.Contracts.ICandidateRepository,PortBlog.API.Repositories.Contracts.IResumeRepository,AutoMapper.IMapper)">
|
||||||
|
<summary>
|
||||||
|
Service for administrative operations related to candidates, resumes, and their associated data.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
<member name="T:PortBlog.API.Services.AuthService">
|
<member name="T:PortBlog.API.Services.AuthService">
|
||||||
<summary>
|
<summary>
|
||||||
Provides authentication services such as OTP generation, validation, and JWT token management.
|
Provides authentication services such as OTP generation, validation, and JWT token management.
|
||||||
@ -493,6 +585,11 @@
|
|||||||
<param name="refreshToken">The refresh token to revoke.</param>
|
<param name="refreshToken">The refresh token to revoke.</param>
|
||||||
<returns>A task that represents the asynchronous revoke operation.</returns>
|
<returns>A task that represents the asynchronous revoke operation.</returns>
|
||||||
</member>
|
</member>
|
||||||
|
<member name="T:PortBlog.API.Services.Contracts.IAdminService">
|
||||||
|
<summary>
|
||||||
|
Service for administrative operations related to candidates, resumes, and their associated data.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
<member name="T:PortBlog.API.Services.Contracts.IAuthService">
|
<member name="T:PortBlog.API.Services.Contracts.IAuthService">
|
||||||
<summary>
|
<summary>
|
||||||
Provides authentication-related services such as OTP generation, token management, and refresh token handling.
|
Provides authentication-related services such as OTP generation, token management, and refresh token handling.
|
||||||
|
|||||||
@ -35,6 +35,16 @@ namespace PortBlog.API.Profiles
|
|||||||
(
|
(
|
||||||
dest => dest.EndYear,
|
dest => dest.EndYear,
|
||||||
opts => opts.MapFrom(src => src.EndDate != null ? src.EndDate.Value.Year.ToString() : "Present")
|
opts => opts.MapFrom(src => src.EndDate != null ? src.EndDate.Value.Year.ToString() : "Present")
|
||||||
|
)
|
||||||
|
.ForMember
|
||||||
|
(
|
||||||
|
dest => dest.StartDate,
|
||||||
|
opts => opts.MapFrom(src => src.StartDate)
|
||||||
|
)
|
||||||
|
.ForMember
|
||||||
|
(
|
||||||
|
dest => dest.EndDate,
|
||||||
|
opts => opts.MapFrom(src => src.EndDate)
|
||||||
);
|
);
|
||||||
CreateMap<ExperienceDetails, ExperienceDetailsDto>();
|
CreateMap<ExperienceDetails, ExperienceDetailsDto>();
|
||||||
CreateMap<Project, ProjectDto>()
|
CreateMap<Project, ProjectDto>()
|
||||||
|
|||||||
@ -99,16 +99,18 @@ builder.Services.AddAutoMapper(cfg => cfg.AddMaps(AppDomain.CurrentDomain.GetAss
|
|||||||
builder.Services.AddCache(builder.Configuration);
|
builder.Services.AddCache(builder.Configuration);
|
||||||
|
|
||||||
|
|
||||||
// Registering API Versioning Specification services
|
builder.Services
|
||||||
builder.Services.AddApiVersioning(setupAction =>
|
.AddApiVersioning(options =>
|
||||||
{
|
{
|
||||||
setupAction.ReportApiVersions = true;
|
options.DefaultApiVersion = new ApiVersion(1, 0);
|
||||||
setupAction.AssumeDefaultVersionWhenUnspecified = true;
|
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||||
setupAction.DefaultApiVersion = new ApiVersion(1, 0);
|
options.ReportApiVersions = true;
|
||||||
}).AddMvc()
|
})
|
||||||
.AddApiExplorer(setupAction =>
|
.AddMvc() // IMPORTANT
|
||||||
|
.AddApiExplorer(options =>
|
||||||
{
|
{
|
||||||
setupAction.SubstituteApiVersionInUrl = true;
|
options.GroupNameFormat = "'v'V";
|
||||||
|
options.SubstituteApiVersionInUrl = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddSwaggerGen(c =>
|
builder.Services.AddSwaggerGen(c =>
|
||||||
@ -168,6 +170,8 @@ builder.Services.AddSwaggerGen(c =>
|
|||||||
c.AddSecurityRequirement(requirement);
|
c.AddSecurityRequirement(requirement);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();
|
||||||
|
|
||||||
var jwtKey = builder.Configuration["Jwt:Key"];
|
var jwtKey = builder.Configuration["Jwt:Key"];
|
||||||
if (string.IsNullOrEmpty(jwtKey))
|
if (string.IsNullOrEmpty(jwtKey))
|
||||||
{
|
{
|
||||||
@ -196,8 +200,6 @@ builder.Services.AddAuthentication(options =>
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseCors();
|
|
||||||
|
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler();
|
app.UseExceptionHandler();
|
||||||
@ -211,6 +213,12 @@ if (app.Environment.IsDevelopment())
|
|||||||
{
|
{
|
||||||
// Get the IApiVersionDescriptionProvider from the app's service provider
|
// Get the IApiVersionDescriptionProvider from the app's service provider
|
||||||
var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
|
var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
|
||||||
|
|
||||||
|
foreach (var d in apiVersionDescriptionProvider.ApiVersionDescriptions)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"GroupName: {d.GroupName}");
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
|
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
|
||||||
{
|
{
|
||||||
setupAction.SwaggerEndpoint(
|
setupAction.SwaggerEndpoint(
|
||||||
@ -229,12 +237,16 @@ app.UseStaticFiles(new StaticFileOptions()
|
|||||||
RequestPath = new PathString("/images")
|
RequestPath = new PathString("/images")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseCors();
|
||||||
|
|
||||||
|
app.UseMiddleware<ApiKeyMiddleware>();
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.UseMiddleware<ApiKeyMiddleware>();
|
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@ -15,5 +15,72 @@ namespace PortBlog.API.Repositories.Contracts
|
|||||||
Task<Resume?> GetProjectsAsync(int candidateId);
|
Task<Resume?> GetProjectsAsync(int candidateId);
|
||||||
|
|
||||||
Task<Blog?> GetBlogAsync(int candidate);
|
Task<Blog?> GetBlogAsync(int candidate);
|
||||||
|
|
||||||
|
// Added for admin write operations used by controllers
|
||||||
|
Task<Resume?> GetByIdAsync(int resumeId);
|
||||||
|
|
||||||
|
Task<Resume?> GetByIdWithCollectionsAsync(int resumeId);
|
||||||
|
|
||||||
|
Task<Project?> GetProjectAsync(int projectId);
|
||||||
|
|
||||||
|
Task<Hobby?> GetHobbyAsync(int hobbyId);
|
||||||
|
|
||||||
|
Task<IEnumerable<Hobby>> GetHobbiesByResumeIdAsync(int resumeId);
|
||||||
|
|
||||||
|
Task<Candidate?> GetCandidateByIdAsync(int candidateId);
|
||||||
|
|
||||||
|
void AddProject(Project project);
|
||||||
|
|
||||||
|
void UpdateProject(Project project);
|
||||||
|
|
||||||
|
void RemoveProject(Project project);
|
||||||
|
|
||||||
|
void AddHobby(Hobby hobby);
|
||||||
|
|
||||||
|
void UpdateHobby(Hobby hobby);
|
||||||
|
|
||||||
|
void RemoveHobby(Hobby hobby);
|
||||||
|
|
||||||
|
void AddSkill(Skill skill);
|
||||||
|
|
||||||
|
void UpdateSkill(Skill skill);
|
||||||
|
|
||||||
|
void RemoveSkill(Skill skill);
|
||||||
|
|
||||||
|
void AddAcademic(Academic academic);
|
||||||
|
|
||||||
|
void UpdateAcademic(Academic academic);
|
||||||
|
|
||||||
|
void RemoveAcademic(Academic academic);
|
||||||
|
|
||||||
|
void AddExperience(Experience experience);
|
||||||
|
|
||||||
|
void UpdateExperience(Experience experience);
|
||||||
|
|
||||||
|
void RemoveExperience(Experience experience);
|
||||||
|
|
||||||
|
void AddExperienceDetails(ExperienceDetails details);
|
||||||
|
|
||||||
|
void UpdateExperienceDetails(ExperienceDetails details);
|
||||||
|
|
||||||
|
void RemoveExperienceDetails(ExperienceDetails details);
|
||||||
|
|
||||||
|
void AddCertification(Certification certification);
|
||||||
|
|
||||||
|
void UpdateCertification(Certification certification);
|
||||||
|
|
||||||
|
void RemoveCertification(Certification certification);
|
||||||
|
|
||||||
|
void AddResume(Resume resume);
|
||||||
|
|
||||||
|
void UpdateResume(Resume resume);
|
||||||
|
|
||||||
|
void UpdateCandidate(Candidate candidate);
|
||||||
|
|
||||||
|
void AddSocialLink(SocialLinks socialLink);
|
||||||
|
|
||||||
|
void UpdateSocialLink(SocialLinks socialLink);
|
||||||
|
|
||||||
|
Task<bool> SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using PortBlog.API.DbContexts;
|
using PortBlog.API.DbContexts;
|
||||||
using PortBlog.API.Entities;
|
using PortBlog.API.Entities;
|
||||||
using PortBlog.API.Models;
|
|
||||||
using PortBlog.API.Repositories.Contracts;
|
using PortBlog.API.Repositories.Contracts;
|
||||||
|
|
||||||
namespace PortBlog.API.Repositories
|
namespace PortBlog.API.Repositories
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Repository for managing resume-related data access and operations.
|
||||||
|
/// </summary>
|
||||||
public class ResumeRepository : IResumeRepository
|
public class ResumeRepository : IResumeRepository
|
||||||
{
|
{
|
||||||
private readonly CvBlogContext _cvBlogContext;
|
private readonly CvBlogContext _cvBlogContext;
|
||||||
@ -14,6 +16,13 @@ namespace PortBlog.API.Repositories
|
|||||||
{
|
{
|
||||||
_cvBlogContext = cvBlogContext;
|
_cvBlogContext = cvBlogContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the latest resume for a candidate, optionally including related data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="candidateId">The ID of the candidate.</param>
|
||||||
|
/// <param name="includeOtherData">If true, includes related data such as academics, experiences, social links, etc.</param>
|
||||||
|
/// <returns>The latest <see cref="Resume"/> for the candidate, or null if not found.</returns>
|
||||||
public async Task<Resume?> GetLatestResumeForCandidateAsync(int candidateId, bool includeOtherData)
|
public async Task<Resume?> GetLatestResumeForCandidateAsync(int candidateId, bool includeOtherData)
|
||||||
{
|
{
|
||||||
if (includeOtherData)
|
if (includeOtherData)
|
||||||
@ -83,5 +92,180 @@ namespace PortBlog.API.Repositories
|
|||||||
|
|
||||||
return result?.SocialLinks?.Blog;
|
return result?.SocialLinks?.Blog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New read helpers
|
||||||
|
public async Task<Resume?> GetByIdAsync(int resumeId)
|
||||||
|
{
|
||||||
|
return await _cvBlogContext.Resumes.FirstOrDefaultAsync(r => r.ResumeId == resumeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Resume?> GetByIdWithCollectionsAsync(int resumeId)
|
||||||
|
{
|
||||||
|
return await _cvBlogContext.Resumes
|
||||||
|
.Include(r => r.Skills)
|
||||||
|
.Include(r => r.Academics)
|
||||||
|
.Include(r => r.Experiences)
|
||||||
|
.ThenInclude(e => e.Details)
|
||||||
|
.Include(r => r.Certifications)
|
||||||
|
.Include(r => r.Projects)
|
||||||
|
.FirstOrDefaultAsync(r => r.ResumeId == resumeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Project?> GetProjectAsync(int projectId)
|
||||||
|
{
|
||||||
|
return await _cvBlogContext.Projects.FirstOrDefaultAsync(p => p.ProjectId == projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Hobby?> GetHobbyAsync(int hobbyId)
|
||||||
|
{
|
||||||
|
return await _cvBlogContext.Hobbies.FirstOrDefaultAsync(h => h.HobbyId == hobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Hobby>> GetHobbiesByResumeIdAsync(int resumeId)
|
||||||
|
{
|
||||||
|
return await _cvBlogContext.Hobbies.Where(h => h.ResumeId == resumeId).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Candidate?> GetCandidateByIdAsync(int candidateId)
|
||||||
|
{
|
||||||
|
return await _cvBlogContext.Candidates
|
||||||
|
.FirstOrDefaultAsync(c => c.CandidateId == candidateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// New write helpers
|
||||||
|
public void AddProject(Project project)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Projects.Add(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateProject(Project project)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Projects.Update(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveProject(Project project)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Projects.Remove(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddHobby(Hobby hobby)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Hobbies.Add(hobby);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateHobby(Hobby hobby)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Hobbies.Update(hobby);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveHobby(Hobby hobby)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Hobbies.Remove(hobby);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddSkill(Skill skill)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Skills.Add(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateSkill(Skill skill)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Skills.Update(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSkill(Skill skill)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Skills.Remove(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAcademic(Academic academic)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Academics.Add(academic);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateAcademic(Academic academic)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Academics.Update(academic);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAcademic(Academic academic)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Academics.Remove(academic);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddExperience(Experience experience)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Experiences.Add(experience);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateExperience(Experience experience)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Experiences.Update(experience);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveExperience(Experience experience)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Experiences.Remove(experience);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddExperienceDetails(ExperienceDetails details)
|
||||||
|
{
|
||||||
|
_cvBlogContext.ExperienceDetails.Add(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateExperienceDetails(ExperienceDetails details)
|
||||||
|
{
|
||||||
|
_cvBlogContext.ExperienceDetails.Update(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveExperienceDetails(ExperienceDetails details)
|
||||||
|
{
|
||||||
|
_cvBlogContext.ExperienceDetails.Remove(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCertification(Certification certification)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Certifications.Add(certification);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateCertification(Certification certification)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Certifications.Update(certification);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveCertification(Certification certification)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Certifications.Remove(certification);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddResume(Resume resume)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Resumes.Add(resume);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateResume(Resume resume)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Resumes.Update(resume);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateCandidate(Candidate candidate)
|
||||||
|
{
|
||||||
|
_cvBlogContext.Candidates.Update(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddSocialLink(SocialLinks socialLink)
|
||||||
|
{
|
||||||
|
_cvBlogContext.SocialLinks.Add(socialLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateSocialLink(SocialLinks socialLink)
|
||||||
|
{
|
||||||
|
_cvBlogContext.SocialLinks.Update(socialLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SaveChangesAsync()
|
||||||
|
{
|
||||||
|
return (await _cvBlogContext.SaveChangesAsync() >= 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
552
PortBlog.API/Services/AdminService.cs
Normal file
552
PortBlog.API/Services/AdminService.cs
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using AutoMapper;
|
||||||
|
using PortBlog.API.Entities;
|
||||||
|
using PortBlog.API.Models;
|
||||||
|
using PortBlog.API.Repositories.Contracts;
|
||||||
|
using PortBlog.API.Services.Contracts;
|
||||||
|
|
||||||
|
namespace PortBlog.API.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service for administrative operations related to candidates, resumes, and their associated data.
|
||||||
|
/// </summary>
|
||||||
|
public class AdminService(ICandidateRepository candidateRepository, IResumeRepository resumeRepository, IMapper mapper) : IAdminService
|
||||||
|
{
|
||||||
|
public int GetCandidateIdFromClaims(ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var candidateIdClaim = user.FindFirst("CandidateId")?.Value;
|
||||||
|
if (string.IsNullOrEmpty(candidateIdClaim) || !int.TryParse(candidateIdClaim, out var candidateId))
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("CandidateId claim is missing or invalid.");
|
||||||
|
}
|
||||||
|
return candidateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AboutDto?> GetHobbiesAsync(int candidateId)
|
||||||
|
{
|
||||||
|
var aboutDetails = await resumeRepository.GetHobbiesAsync(candidateId);
|
||||||
|
return aboutDetails != null ? mapper.Map<AboutDto>(aboutDetails) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CandidateSocialLinksDto?> GetContactAsync(int candidateId)
|
||||||
|
{
|
||||||
|
var contact = await resumeRepository.GetCandidateWithSocialLinksAsync(candidateId);
|
||||||
|
return contact != null ? mapper.Map<CandidateSocialLinksDto>(contact) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ResumeDto?> GetResumeAsync(int candidateId)
|
||||||
|
{
|
||||||
|
var resume = await resumeRepository.GetResumeAsync(candidateId);
|
||||||
|
return resume != null ? mapper.Map<ResumeDto>(resume) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProjectsDto?> GetProjectsAsync(int candidateId)
|
||||||
|
{
|
||||||
|
var projects = await resumeRepository.GetProjectsAsync(candidateId);
|
||||||
|
return projects != null ? mapper.Map<ProjectsDto>(projects) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProjectDto> UpsertProjectAsync(int candidateId, ProjectDto projectDto)
|
||||||
|
{
|
||||||
|
var resumeWithCollections = await GetOrCreateResumeWithCollectionsAsync(candidateId);
|
||||||
|
|
||||||
|
Project? projectEntity = null;
|
||||||
|
if (projectDto.ProjectId > 0)
|
||||||
|
{
|
||||||
|
projectEntity = await resumeRepository.GetProjectAsync(projectDto.ProjectId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectEntity == null)
|
||||||
|
{
|
||||||
|
projectEntity = new Project
|
||||||
|
{
|
||||||
|
Name = projectDto.Name,
|
||||||
|
Description = projectDto.Description,
|
||||||
|
Categories = projectDto.Categories ?? Array.Empty<string>(),
|
||||||
|
Roles = projectDto.Roles != null ? string.Join(",", projectDto.Roles) : null,
|
||||||
|
Responsibilities = projectDto.Responsibilities != null ? string.Join(",", projectDto.Responsibilities) : null,
|
||||||
|
TechnologiesUsed = projectDto.TechnologiesUsed != null ? string.Join(",", projectDto.TechnologiesUsed) : null,
|
||||||
|
Challenges = projectDto.Challenges,
|
||||||
|
LessonsLearned = projectDto.LessonsLearned,
|
||||||
|
Impact = projectDto.Impact,
|
||||||
|
StartDate = projectDto.StartDate == default ? null : projectDto.StartDate,
|
||||||
|
EndDate = projectDto.EndDate == default ? null : projectDto.EndDate,
|
||||||
|
ImagePath = projectDto.ImagePath,
|
||||||
|
Status = projectDto.Status,
|
||||||
|
ResumeId = resumeWithCollections.ResumeId
|
||||||
|
};
|
||||||
|
|
||||||
|
resumeRepository.AddProject(projectEntity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
projectEntity.Name = projectDto.Name;
|
||||||
|
projectEntity.Description = projectDto.Description;
|
||||||
|
projectEntity.Categories = projectDto.Categories ?? Array.Empty<string>();
|
||||||
|
projectEntity.Roles = projectDto.Roles != null ? string.Join(",", projectDto.Roles) : projectEntity.Roles;
|
||||||
|
projectEntity.Responsibilities = projectDto.Responsibilities != null ? string.Join(",", projectDto.Responsibilities) : projectEntity.Responsibilities;
|
||||||
|
projectEntity.TechnologiesUsed = projectDto.TechnologiesUsed != null ? string.Join(",", projectDto.TechnologiesUsed) : projectEntity.TechnologiesUsed;
|
||||||
|
projectEntity.Challenges = projectDto.Challenges;
|
||||||
|
projectEntity.LessonsLearned = projectDto.LessonsLearned;
|
||||||
|
projectEntity.Impact = projectDto.Impact;
|
||||||
|
projectEntity.StartDate = projectDto.StartDate == default ? projectEntity.StartDate : projectDto.StartDate;
|
||||||
|
projectEntity.EndDate = projectDto.EndDate == default ? projectEntity.EndDate : projectDto.EndDate;
|
||||||
|
projectEntity.ImagePath = projectDto.ImagePath;
|
||||||
|
projectEntity.Status = projectDto.Status;
|
||||||
|
|
||||||
|
resumeRepository.UpdateProject(projectEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
await resumeRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
return mapper.Map<ProjectDto>(projectEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteProjectAsync(int candidateId, int projectId)
|
||||||
|
{
|
||||||
|
var resume = await resumeRepository.GetLatestResumeForCandidateAsync(candidateId, includeOtherData: false)
|
||||||
|
?? throw new KeyNotFoundException($"Resume for candidate {candidateId} not found.");
|
||||||
|
|
||||||
|
var project = await resumeRepository.GetProjectAsync(projectId);
|
||||||
|
if (project == null || project.ResumeId != resume.ResumeId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeRepository.RemoveProject(project);
|
||||||
|
await resumeRepository.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AboutDto> UpsertHobbiesAsync(int candidateId, AboutDto aboutDto)
|
||||||
|
{
|
||||||
|
var resume = await resumeRepository.GetLatestResumeForCandidateAsync(candidateId, includeOtherData: false)
|
||||||
|
?? throw new KeyNotFoundException($"Resume for candidate {candidateId} not found.");
|
||||||
|
|
||||||
|
// Update About
|
||||||
|
if (!string.IsNullOrWhiteSpace(aboutDto.About))
|
||||||
|
{
|
||||||
|
resume.About = aboutDto.About;
|
||||||
|
resumeRepository.UpdateResume(resume);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert Hobbies
|
||||||
|
var existingHobbies = (await resumeRepository.GetHobbiesByResumeIdAsync(resume.ResumeId)).ToList();
|
||||||
|
var incomingIds = aboutDto.Hobbies.Where(h => h.HobbyId > 0).Select(h => h.HobbyId).ToHashSet();
|
||||||
|
|
||||||
|
foreach (var existing in existingHobbies)
|
||||||
|
{
|
||||||
|
if (!incomingIds.Contains(existing.HobbyId))
|
||||||
|
{
|
||||||
|
resumeRepository.RemoveHobby(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var order = 1;
|
||||||
|
var resultEntities = new List<Hobby>();
|
||||||
|
|
||||||
|
foreach (var hobbyDto in aboutDto.Hobbies)
|
||||||
|
{
|
||||||
|
Hobby? hobbyEntity = null;
|
||||||
|
|
||||||
|
if (hobbyDto.HobbyId > 0)
|
||||||
|
{
|
||||||
|
hobbyEntity = existingHobbies.FirstOrDefault(h => h.HobbyId == hobbyDto.HobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hobbyEntity == null)
|
||||||
|
{
|
||||||
|
hobbyEntity = new Hobby
|
||||||
|
{
|
||||||
|
Name = hobbyDto.Name,
|
||||||
|
Description = hobbyDto.Description,
|
||||||
|
Icon = hobbyDto.Icon,
|
||||||
|
ResumeId = resume.ResumeId,
|
||||||
|
Order = order
|
||||||
|
};
|
||||||
|
resumeRepository.AddHobby(hobbyEntity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hobbyEntity.Name = hobbyDto.Name;
|
||||||
|
hobbyEntity.Description = hobbyDto.Description;
|
||||||
|
hobbyEntity.Icon = hobbyDto.Icon;
|
||||||
|
hobbyEntity.Order = order;
|
||||||
|
resumeRepository.UpdateHobby(hobbyEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
resultEntities.Add(hobbyEntity);
|
||||||
|
order++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await resumeRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new AboutDto
|
||||||
|
{
|
||||||
|
About = resume.About,
|
||||||
|
Hobbies = mapper.Map<ICollection<HobbyDto>>(resultEntities)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CandidateSocialLinksDto> UpsertContactAsync(int candidateId, CandidateSocialLinksDto contactDto)
|
||||||
|
{
|
||||||
|
var resume = await resumeRepository.GetCandidateWithSocialLinksAsync(candidateId)
|
||||||
|
?? throw new KeyNotFoundException($"Resume with candidate id {candidateId} not found.");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(contactDto.Title))
|
||||||
|
{
|
||||||
|
resume.Title = contactDto.Title;
|
||||||
|
resumeRepository.UpdateResume(resume);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contactDto.Candidate != null && resume.Candidate != null)
|
||||||
|
{
|
||||||
|
resume.Candidate.FirstName = contactDto.Candidate.FirstName;
|
||||||
|
resume.Candidate.LastName = contactDto.Candidate.LastName;
|
||||||
|
resume.Candidate.Email = contactDto.Candidate.Email;
|
||||||
|
resume.Candidate.Phone = contactDto.Candidate.Phone;
|
||||||
|
resume.Candidate.Address = contactDto.Candidate.Address;
|
||||||
|
|
||||||
|
resumeRepository.UpdateCandidate(resume.Candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contactDto.SocialLinks != null)
|
||||||
|
{
|
||||||
|
if (resume.SocialLinks == null)
|
||||||
|
{
|
||||||
|
resume.SocialLinks = new SocialLinks
|
||||||
|
{
|
||||||
|
ResumeId = resume.ResumeId,
|
||||||
|
GitHub = contactDto.SocialLinks.GitHub,
|
||||||
|
Linkedin = contactDto.SocialLinks.Linkedin,
|
||||||
|
Instagram = contactDto.SocialLinks.Instagram,
|
||||||
|
Facebook = contactDto.SocialLinks.Facebook,
|
||||||
|
Twitter = contactDto.SocialLinks.Twitter,
|
||||||
|
PersonalWebsite = contactDto.SocialLinks.PersonalWebsite,
|
||||||
|
BlogUrl = contactDto.SocialLinks.BlogUrl
|
||||||
|
};
|
||||||
|
resumeRepository.AddSocialLink(resume.SocialLinks);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resume.SocialLinks.GitHub = contactDto.SocialLinks.GitHub;
|
||||||
|
resume.SocialLinks.Linkedin = contactDto.SocialLinks.Linkedin;
|
||||||
|
resume.SocialLinks.Instagram = contactDto.SocialLinks.Instagram;
|
||||||
|
resume.SocialLinks.Facebook = contactDto.SocialLinks.Facebook;
|
||||||
|
resume.SocialLinks.Twitter = contactDto.SocialLinks.Twitter;
|
||||||
|
resume.SocialLinks.PersonalWebsite = contactDto.SocialLinks.PersonalWebsite;
|
||||||
|
resume.SocialLinks.BlogUrl = contactDto.SocialLinks.BlogUrl;
|
||||||
|
|
||||||
|
resumeRepository.UpdateSocialLink(resume.SocialLinks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await resumeRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
var updatedContact = await resumeRepository.GetCandidateWithSocialLinksAsync(candidateId);
|
||||||
|
return mapper.Map<CandidateSocialLinksDto>(updatedContact);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<SkillDto>> UpsertSkillsAsync(int candidateId, IEnumerable<SkillDto> skillDtos)
|
||||||
|
{
|
||||||
|
var resumeWithCollections = await GetOrCreateResumeWithCollectionsAsync(candidateId);
|
||||||
|
|
||||||
|
UpsertSkills(resumeWithCollections, skillDtos.ToList());
|
||||||
|
|
||||||
|
await resumeRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
var updatedResume = await resumeRepository.GetByIdWithCollectionsAsync(resumeWithCollections.ResumeId);
|
||||||
|
return mapper.Map<IEnumerable<SkillDto>>(updatedResume?.Skills ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<AcademicDto>> UpsertAcademicsAsync(int candidateId, IEnumerable<AcademicDto> academicDtos)
|
||||||
|
{
|
||||||
|
var resumeWithCollections = await GetOrCreateResumeWithCollectionsAsync(candidateId);
|
||||||
|
|
||||||
|
UpsertAcademics(resumeWithCollections, academicDtos.ToList());
|
||||||
|
|
||||||
|
await resumeRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
var updatedResume = await resumeRepository.GetByIdWithCollectionsAsync(resumeWithCollections.ResumeId);
|
||||||
|
return mapper.Map<IEnumerable<AcademicDto>>(updatedResume?.Academics ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ExperienceDto>> UpsertExperiencesAsync(int candidateId, IEnumerable<ExperienceDto> experienceDtos)
|
||||||
|
{
|
||||||
|
var resumeWithCollections = await GetOrCreateResumeWithCollectionsAsync(candidateId);
|
||||||
|
|
||||||
|
UpsertExperiences(resumeWithCollections, experienceDtos.ToList());
|
||||||
|
|
||||||
|
await resumeRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
var updatedResume = await resumeRepository.GetByIdWithCollectionsAsync(resumeWithCollections.ResumeId);
|
||||||
|
return mapper.Map<IEnumerable<ExperienceDto>>(updatedResume?.Experiences ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CertificationDto>> UpsertCertificationsAsync(int candidateId, IEnumerable<CertificationDto> certificationDtos)
|
||||||
|
{
|
||||||
|
var resumeWithCollections = await GetOrCreateResumeWithCollectionsAsync(candidateId);
|
||||||
|
|
||||||
|
UpsertCertifications(resumeWithCollections, certificationDtos.ToList());
|
||||||
|
|
||||||
|
await resumeRepository.SaveChangesAsync();
|
||||||
|
|
||||||
|
var updatedResume = await resumeRepository.GetByIdWithCollectionsAsync(resumeWithCollections.ResumeId);
|
||||||
|
return mapper.Map<IEnumerable<CertificationDto>>(updatedResume?.Certifications ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Private Helpers
|
||||||
|
|
||||||
|
private async Task<Resume> GetOrCreateResumeWithCollectionsAsync(int candidateId)
|
||||||
|
{
|
||||||
|
var resume = await resumeRepository.GetLatestResumeForCandidateAsync(candidateId, includeOtherData: false);
|
||||||
|
|
||||||
|
if (resume == null)
|
||||||
|
{
|
||||||
|
resume = new Resume
|
||||||
|
{
|
||||||
|
CandidateId = candidateId,
|
||||||
|
About = string.Empty,
|
||||||
|
Order = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
resumeRepository.AddResume(resume);
|
||||||
|
await resumeRepository.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await resumeRepository.GetByIdWithCollectionsAsync(resume.ResumeId)
|
||||||
|
?? throw new InvalidOperationException("Failed to reload resume with collections.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpsertSkills(Resume resume, ICollection<SkillDto> skillDtos)
|
||||||
|
{
|
||||||
|
var existingSkills = resume.Skills.ToList();
|
||||||
|
var incomingIds = skillDtos.Where(s => s.SkillId > 0).Select(s => s.SkillId).ToHashSet();
|
||||||
|
|
||||||
|
foreach (var existing in existingSkills)
|
||||||
|
{
|
||||||
|
if (!incomingIds.Contains(existing.SkillId))
|
||||||
|
{
|
||||||
|
resumeRepository.RemoveSkill(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var skillDto in skillDtos)
|
||||||
|
{
|
||||||
|
var skillEntity = skillDto.SkillId > 0
|
||||||
|
? existingSkills.FirstOrDefault(s => s.SkillId == skillDto.SkillId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (skillEntity == null)
|
||||||
|
{
|
||||||
|
resumeRepository.AddSkill(new Skill
|
||||||
|
{
|
||||||
|
Name = skillDto.Name,
|
||||||
|
Description = skillDto.Description,
|
||||||
|
ProficiencyLevel = skillDto.ProficiencyLevel,
|
||||||
|
ResumeId = resume.ResumeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
skillEntity.Name = skillDto.Name;
|
||||||
|
skillEntity.Description = skillDto.Description;
|
||||||
|
skillEntity.ProficiencyLevel = skillDto.ProficiencyLevel;
|
||||||
|
resumeRepository.UpdateSkill(skillEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpsertAcademics(Resume resume, ICollection<AcademicDto> academicDtos)
|
||||||
|
{
|
||||||
|
var existingAcademics = resume.Academics.ToList();
|
||||||
|
var incomingIds = academicDtos.Where(a => a.AcademicId > 0).Select(a => a.AcademicId).ToHashSet();
|
||||||
|
|
||||||
|
foreach (var existing in existingAcademics)
|
||||||
|
{
|
||||||
|
if (!incomingIds.Contains(existing.AcademicId))
|
||||||
|
{
|
||||||
|
resumeRepository.RemoveAcademic(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var academicDto in academicDtos)
|
||||||
|
{
|
||||||
|
var academicEntity = academicDto.AcademicId > 0
|
||||||
|
? existingAcademics.FirstOrDefault(a => a.AcademicId == academicDto.AcademicId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (academicEntity == null)
|
||||||
|
{
|
||||||
|
resumeRepository.AddAcademic(new Academic
|
||||||
|
{
|
||||||
|
Institution = academicDto.Institution,
|
||||||
|
StartYear = academicDto.StartYear,
|
||||||
|
EndYear = academicDto.EndYear,
|
||||||
|
Degree = academicDto.Degree,
|
||||||
|
DegreeSpecialization = academicDto.DegreeSpecialization,
|
||||||
|
ResumeId = resume.ResumeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
academicEntity.Institution = academicDto.Institution;
|
||||||
|
academicEntity.StartYear = academicDto.StartYear;
|
||||||
|
academicEntity.EndYear = academicDto.EndYear;
|
||||||
|
academicEntity.Degree = academicDto.Degree;
|
||||||
|
academicEntity.DegreeSpecialization = academicDto.DegreeSpecialization;
|
||||||
|
resumeRepository.UpdateAcademic(academicEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpsertExperiences(Resume resume, ICollection<ExperienceDto> experienceDtos)
|
||||||
|
{
|
||||||
|
var existingExperiences = resume.Experiences.ToList();
|
||||||
|
var incomingIds = experienceDtos.Where(e => e.ExperienceId > 0).Select(e => e.ExperienceId).ToHashSet();
|
||||||
|
|
||||||
|
foreach (var existing in existingExperiences)
|
||||||
|
{
|
||||||
|
if (!incomingIds.Contains(existing.ExperienceId))
|
||||||
|
{
|
||||||
|
foreach (var detail in existing.Details.ToList())
|
||||||
|
{
|
||||||
|
resumeRepository.RemoveExperienceDetails(detail);
|
||||||
|
}
|
||||||
|
resumeRepository.RemoveExperience(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var experienceDto in experienceDtos)
|
||||||
|
{
|
||||||
|
var experienceEntity = experienceDto.ExperienceId > 0
|
||||||
|
? existingExperiences.FirstOrDefault(e => e.ExperienceId == experienceDto.ExperienceId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (experienceEntity == null)
|
||||||
|
{
|
||||||
|
experienceEntity = new Experience
|
||||||
|
{
|
||||||
|
Title = experienceDto.Title,
|
||||||
|
Description = experienceDto.Description,
|
||||||
|
Company = experienceDto.Company,
|
||||||
|
Location = experienceDto.Location ?? string.Empty,
|
||||||
|
StartDate = experienceDto.StartDate ?? DateTime.UtcNow,
|
||||||
|
EndDate = experienceDto.EndDate,
|
||||||
|
ResumeId = resume.ResumeId
|
||||||
|
};
|
||||||
|
resumeRepository.AddExperience(experienceEntity);
|
||||||
|
|
||||||
|
var detailOrder = 1;
|
||||||
|
foreach (var detailDto in experienceDto.Details)
|
||||||
|
{
|
||||||
|
resumeRepository.AddExperienceDetails(new ExperienceDetails
|
||||||
|
{
|
||||||
|
Details = detailDto.Details,
|
||||||
|
Order = detailOrder++,
|
||||||
|
Experience = experienceEntity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
experienceEntity.Title = experienceDto.Title;
|
||||||
|
experienceEntity.Description = experienceDto.Description;
|
||||||
|
experienceEntity.Company = experienceDto.Company;
|
||||||
|
if (!string.IsNullOrWhiteSpace(experienceDto.Location))
|
||||||
|
experienceEntity.Location = experienceDto.Location;
|
||||||
|
if (experienceDto.StartDate.HasValue)
|
||||||
|
experienceEntity.StartDate = experienceDto.StartDate.Value;
|
||||||
|
experienceEntity.EndDate = experienceDto.EndDate;
|
||||||
|
|
||||||
|
resumeRepository.UpdateExperience(experienceEntity);
|
||||||
|
UpsertExperienceDetails(experienceEntity, experienceDto.Details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpsertExperienceDetails(Experience experience, ICollection<ExperienceDetailsDto> detailDtos)
|
||||||
|
{
|
||||||
|
var existingDetails = experience.Details.ToList();
|
||||||
|
var incomingIds = detailDtos.Where(d => d.Id > 0).Select(d => d.Id).ToHashSet();
|
||||||
|
|
||||||
|
foreach (var existing in existingDetails)
|
||||||
|
{
|
||||||
|
if (!incomingIds.Contains(existing.Id))
|
||||||
|
{
|
||||||
|
resumeRepository.RemoveExperienceDetails(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var order = 1;
|
||||||
|
foreach (var detailDto in detailDtos)
|
||||||
|
{
|
||||||
|
var detailEntity = detailDto.Id > 0
|
||||||
|
? existingDetails.FirstOrDefault(d => d.Id == detailDto.Id)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (detailEntity == null)
|
||||||
|
{
|
||||||
|
resumeRepository.AddExperienceDetails(new ExperienceDetails
|
||||||
|
{
|
||||||
|
Details = detailDto.Details,
|
||||||
|
Order = order,
|
||||||
|
ExperienceId = experience.ExperienceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
detailEntity.Details = detailDto.Details;
|
||||||
|
detailEntity.Order = order;
|
||||||
|
resumeRepository.UpdateExperienceDetails(detailEntity);
|
||||||
|
}
|
||||||
|
order++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpsertCertifications(Resume resume, ICollection<CertificationDto> certificationDtos)
|
||||||
|
{
|
||||||
|
var existingCertifications = resume.Certifications.ToList();
|
||||||
|
var incomingIds = certificationDtos.Where(c => c.CertificationId > 0).Select(c => c.CertificationId).ToHashSet();
|
||||||
|
|
||||||
|
foreach (var existing in existingCertifications)
|
||||||
|
{
|
||||||
|
if (!incomingIds.Contains(existing.CertificationId))
|
||||||
|
{
|
||||||
|
resumeRepository.RemoveCertification(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var certDto in certificationDtos)
|
||||||
|
{
|
||||||
|
var existingCert = certDto.CertificationId > 0
|
||||||
|
? existingCertifications.FirstOrDefault(c => c.CertificationId == certDto.CertificationId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (existingCert == null)
|
||||||
|
{
|
||||||
|
resumeRepository.AddCertification(new Certification
|
||||||
|
{
|
||||||
|
CertificationName = certDto.CertificationName,
|
||||||
|
IssuingOrganization = certDto.IssuingOrganization,
|
||||||
|
CertificationLink = certDto.CertificationLink,
|
||||||
|
IssueDate = certDto.IssueDate,
|
||||||
|
ExpiryDate = certDto.ExpiryDate,
|
||||||
|
ResumeId = resume.ResumeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingCert.CertificationName = certDto.CertificationName;
|
||||||
|
existingCert.IssuingOrganization = certDto.IssuingOrganization;
|
||||||
|
existingCert.CertificationLink = certDto.CertificationLink;
|
||||||
|
existingCert.IssueDate = certDto.IssueDate;
|
||||||
|
existingCert.ExpiryDate = certDto.ExpiryDate;
|
||||||
|
resumeRepository.UpdateCertification(existingCert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -56,12 +56,19 @@ namespace PortBlog.API.Services
|
|||||||
/// <returns>The generated JWT access token as a string.</returns>
|
/// <returns>The generated JWT access token as a string.</returns>
|
||||||
public string GenerateAccessToken(string username)
|
public string GenerateAccessToken(string username)
|
||||||
{
|
{
|
||||||
var claims = new[]
|
var candidate = context.Candidates.FirstOrDefault(c => c.Email == username);
|
||||||
|
|
||||||
|
var claims = new List<Claim>
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.Name, username),
|
new Claim(ClaimTypes.Name, username),
|
||||||
new Claim(ClaimTypes.Role, "Admin")
|
new Claim(ClaimTypes.Role, "Admin")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (candidate != null)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim("CandidateId", candidate.CandidateId.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
var jwtKey = configuration["Jwt:Key"];
|
var jwtKey = configuration["Jwt:Key"];
|
||||||
if (string.IsNullOrEmpty(jwtKey))
|
if (string.IsNullOrEmpty(jwtKey))
|
||||||
throw new InvalidOperationException("JWT key is not configured.");
|
throw new InvalidOperationException("JWT key is not configured.");
|
||||||
|
|||||||
37
PortBlog.API/Services/Contracts/IAdminService.cs
Normal file
37
PortBlog.API/Services/Contracts/IAdminService.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using PortBlog.API.Models;
|
||||||
|
|
||||||
|
namespace PortBlog.API.Services.Contracts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service for administrative operations related to candidates, resumes, and their associated data.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAdminService
|
||||||
|
{
|
||||||
|
int GetCandidateIdFromClaims(ClaimsPrincipal user);
|
||||||
|
|
||||||
|
Task<AboutDto?> GetHobbiesAsync(int candidateId);
|
||||||
|
|
||||||
|
Task<CandidateSocialLinksDto?> GetContactAsync(int candidateId);
|
||||||
|
|
||||||
|
Task<ResumeDto?> GetResumeAsync(int candidateId);
|
||||||
|
|
||||||
|
Task<ProjectsDto?> GetProjectsAsync(int candidateId);
|
||||||
|
|
||||||
|
Task<ProjectDto> UpsertProjectAsync(int candidateId, ProjectDto projectDto);
|
||||||
|
|
||||||
|
Task<bool> DeleteProjectAsync(int candidateId, int projectId);
|
||||||
|
|
||||||
|
Task<AboutDto> UpsertHobbiesAsync(int candidateId, AboutDto aboutDto);
|
||||||
|
|
||||||
|
Task<IEnumerable<SkillDto>> UpsertSkillsAsync(int candidateId, IEnumerable<SkillDto> skillDtos);
|
||||||
|
|
||||||
|
Task<IEnumerable<AcademicDto>> UpsertAcademicsAsync(int candidateId, IEnumerable<AcademicDto> academicDtos);
|
||||||
|
|
||||||
|
Task<IEnumerable<ExperienceDto>> UpsertExperiencesAsync(int candidateId, IEnumerable<ExperienceDto> experienceDtos);
|
||||||
|
|
||||||
|
Task<IEnumerable<CertificationDto>> UpsertCertificationsAsync(int candidateId, IEnumerable<CertificationDto> certificationDtos);
|
||||||
|
|
||||||
|
Task<CandidateSocialLinksDto> UpsertContactAsync(int candidateId, CandidateSocialLinksDto contactDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
PortBlog.Tests/PortBlog.Tests.csproj
Normal file
21
PortBlog.Tests/PortBlog.Tests.csproj
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loading…
Reference in New Issue
Block a user