feature/portfolio-admin-auth #2
@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.9.34701.34
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.1.11304.174
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PortBlog.API", "PortBlog.API\PortBlog.API.csproj", "{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}"
|
||||
EndProject
|
||||
@ -15,32 +15,90 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KBR.Shared", "Shared\KBR.Sh
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KBR.Shared.Lite", "Shared\KBR.Share.Lite\KBR.Shared.Lite.csproj", "{F4B7078B-C59A-46B8-881A-C3CEE2634498}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PortBlog.Tests", "PortBlog.Tests\PortBlog.Tests.csproj", "{11106F82-FC17-497D-8747-CF1A84BFA7F8}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{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|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.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.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.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.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.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.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.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.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.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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@ -1,150 +1,420 @@
|
||||
using Asp.Versioning;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PortBlog.API.Models;
|
||||
using PortBlog.API.Repositories.Contracts;
|
||||
using PortBlog.API.Services.Contracts;
|
||||
|
||||
namespace PortBlog.API.Controllers
|
||||
{
|
||||
|
||||
/// <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>
|
||||
[Route("api/v{version:apiVersion}/admin")]
|
||||
[ApiController]
|
||||
[ApiVersion(1)]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/admin")]
|
||||
[Authorize]
|
||||
public class AdminController(ILogger<CvController> logger, ICandidateRepository candidateRepository, IResumeRepository resumeRepository, IMapper mapper) : Controller
|
||||
public class AdminController(ILogger<CvController> logger, IAdminService adminService) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Get hobbies of the candidate by candidateid
|
||||
/// Get hobbies of the logged-in candidate.
|
||||
/// </summary>
|
||||
/// <param name="candidateId">The id of the candidate whose hobbies to get</param>
|
||||
/// <returns>Hobbies of the candidate</returns>
|
||||
/// <returns>Hobbies and about details of the candidate</returns>
|
||||
/// <response code="200">Returns the requested hobbies of the candidate</response>
|
||||
[HttpGet("GetHobbies/{candidateId}")]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[HttpGet("GetHobbies")]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<AboutDto>> GetHobbies(int candidateId)
|
||||
public async Task<ActionResult<AboutDto>> GetHobbies()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
||||
{
|
||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching about details.", candidateId);
|
||||
return NotFound();
|
||||
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||
var aboutDetails = await adminService.GetHobbiesAsync(candidateId);
|
||||
return Ok(aboutDetails);
|
||||
}
|
||||
|
||||
var aboutDetails = await resumeRepository.GetHobbiesAsync(candidateId);
|
||||
|
||||
return Ok(mapper.Map<AboutDto>(aboutDetails));
|
||||
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
logger.LogInformation(ex, "Unauthorized access when fetching hobbies.");
|
||||
return Unauthorized(ex.Message);
|
||||
}
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get Candidate details with social links by candidateid
|
||||
/// Get contact details (candidate with social links) for the logged-in candidate.
|
||||
/// </summary>
|
||||
/// <param name="candidateId">The id of the candidate whose detials to get with social links</param>
|
||||
/// <returns>Candidate details with sociallinks</returns>
|
||||
/// <returns>Candidate details with social links</returns>
|
||||
/// <response code="200">Returns the requested candidate details with social links</response>
|
||||
[HttpGet("GetCandidateWithSocialLinks/{candidateId}")]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[HttpGet("GetCandidateWithSocialLinks")]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CandidateSocialLinksDto>> GetContact(int candidateId)
|
||||
public async Task<ActionResult<CandidateSocialLinksDto>> GetContact()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
||||
{
|
||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching candidate with social links.", candidateId);
|
||||
return NotFound();
|
||||
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||
var contact = await adminService.GetContactAsync(candidateId);
|
||||
return Ok(contact);
|
||||
}
|
||||
|
||||
var contact = await resumeRepository.GetCandidateWithSocialLinksAsync(candidateId);
|
||||
|
||||
return Ok(mapper.Map<CandidateSocialLinksDto>(contact));
|
||||
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
logger.LogInformation(ex, "Unauthorized access when fetching contact.");
|
||||
return Unauthorized(ex.Message);
|
||||
}
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get Candidate resume by candidateid
|
||||
/// Get resume for the logged-in candidate.
|
||||
/// </summary>
|
||||
/// <param name="candidateId">The id of the candidate whose resume to get</param>
|
||||
/// <returns>Candidate resume</returns>
|
||||
/// <response code="200">Returns the requested candidate resume</response>
|
||||
[HttpGet("GetResume/{candidateId}")]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[HttpGet("GetResume")]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ResumeDto>> GetResume(int candidateId)
|
||||
public async Task<ActionResult<ResumeDto>> GetResume()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
||||
{
|
||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching resume.", candidateId);
|
||||
return NotFound();
|
||||
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||
var resume = await adminService.GetResumeAsync(candidateId);
|
||||
return Ok(resume);
|
||||
}
|
||||
|
||||
var resume = await resumeRepository.GetResumeAsync(candidateId);
|
||||
|
||||
return Ok(mapper.Map<ResumeDto>(resume));
|
||||
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
logger.LogInformation(ex, "Unauthorized access when fetching resume.");
|
||||
return Unauthorized(ex.Message);
|
||||
}
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get Candidate projects by candidateid
|
||||
/// Get projects for the logged-in candidate.
|
||||
/// </summary>
|
||||
/// <param name="candidateId">The id of the candidate whose projects to get</param>
|
||||
/// <returns>Candidate projects</returns>
|
||||
/// <response code="200">Returns the requested candidate projects</response>
|
||||
[HttpGet("GetProjects/{candidateId}")]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[HttpGet("GetProjects")]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ProjectsDto>> GetProjects(int candidateId)
|
||||
public async Task<ActionResult<ProjectsDto>> GetProjects()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
||||
{
|
||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching projects.", candidateId);
|
||||
return NotFound();
|
||||
var candidateId = adminService.GetCandidateIdFromClaims(User);
|
||||
var projects = await adminService.GetProjectsAsync(candidateId);
|
||||
return Ok(projects);
|
||||
}
|
||||
|
||||
var projects = await resumeRepository.GetProjectsAsync(candidateId);
|
||||
|
||||
return Ok(mapper.Map<ProjectsDto>(projects));
|
||||
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
logger.LogInformation(ex, "Unauthorized access when fetching projects.");
|
||||
return Unauthorized(ex.Message);
|
||||
}
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,9 +12,9 @@ namespace PortBlog.API.Controllers
|
||||
/// <summary>
|
||||
/// Controller for handling authentication-related operations.
|
||||
/// </summary>
|
||||
[Route("api/v{version:apiVersion}/auth")]
|
||||
[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
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@ -10,7 +10,7 @@ namespace PortBlog.API.Controllers
|
||||
{
|
||||
[Route("blog/api/v{version:apiVersion}/posts")]
|
||||
[ApiController]
|
||||
[ApiVersion(1)]
|
||||
[ApiVersion("1.0")]
|
||||
public class BlogController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<BlogController> _logger;
|
||||
|
||||
@ -7,9 +7,9 @@ using PortBlog.API.Services.Contracts;
|
||||
|
||||
namespace PortBlog.API.Controllers
|
||||
{
|
||||
[Route("api/v{versions:apiVersion}/cv")]
|
||||
[ApiController]
|
||||
[ApiVersion(1)]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/cv")]
|
||||
public class CvController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<CvController> _logger;
|
||||
@ -37,7 +37,7 @@ namespace PortBlog.API.Controllers
|
||||
/// <response code="200">Returns the requested cv of the candidate</response>
|
||||
[HttpGet("{candidateId}")]
|
||||
[Obsolete]
|
||||
[ApiVersion(0.1, Deprecated = true)]
|
||||
[ApiVersion("0.1", Deprecated = true)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace PortBlog.API.Controllers
|
||||
{
|
||||
[Route("api/blog/post")]
|
||||
[ApiVersionNeutral]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/blog/post")]
|
||||
public class PostController : ControllerBase
|
||||
{
|
||||
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<IAuthService, AuthService>();
|
||||
services.AddTransient<ITemplateService, TemplateService>();
|
||||
services.AddScoped<IAdminService, AdminService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{
|
||||
public class AcademicDto
|
||||
{
|
||||
public int AcademicId { get; set; }
|
||||
public int? AcademicId { get; set; }
|
||||
|
||||
public string Institution { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{
|
||||
public class CertificationDto
|
||||
{
|
||||
public int CertificationId { get; set; }
|
||||
public int? CertificationId { get; set; }
|
||||
|
||||
public string CertificationName { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{
|
||||
public class ExperienceDetailsDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int? Id { get; set; }
|
||||
|
||||
public string Details { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ namespace PortBlog.API.Models
|
||||
{
|
||||
public class ExperienceDto
|
||||
{
|
||||
public int ExperienceId { get; set; }
|
||||
public int? ExperienceId { get; set; }
|
||||
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
@ -12,10 +12,31 @@ namespace PortBlog.API.Models
|
||||
|
||||
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;
|
||||
|
||||
/// <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;
|
||||
|
||||
/// <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 ICollection<ExperienceDetailsDto> Details { get; set; } = new List<ExperienceDetailsDto>();
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{
|
||||
public class HobbyDto
|
||||
{
|
||||
public int HobbyId { get; set; }
|
||||
public int? HobbyId { get; set; }
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ namespace PortBlog.API.Models
|
||||
{
|
||||
public class ProjectDto
|
||||
{
|
||||
public int ProjectId { get; set; }
|
||||
public int? ProjectId { get; set; }
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{
|
||||
public class SkillDto
|
||||
{
|
||||
public int SkillId { get; set; }
|
||||
public int? SkillId { get; set; }
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.1" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="AutoMapper" Version="15.0.1" />
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
|
||||
@ -6,46 +6,90 @@
|
||||
<members>
|
||||
<member name="T:PortBlog.API.Controllers.AdminController">
|
||||
<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>
|
||||
</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>
|
||||
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>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Controllers.AdminController.GetHobbies(System.Int32)">
|
||||
<member name="M:PortBlog.API.Controllers.AdminController.GetHobbies">
|
||||
<summary>
|
||||
Get hobbies of the candidate by candidateid
|
||||
Get hobbies of the logged-in candidate.
|
||||
</summary>
|
||||
<param name="candidateId">The id of the candidate whose hobbies to get</param>
|
||||
<returns>Hobbies of the candidate</returns>
|
||||
<returns>Hobbies and about details of the candidate</returns>
|
||||
<response code="200">Returns the requested hobbies of the candidate</response>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Controllers.AdminController.GetContact(System.Int32)">
|
||||
<member name="M:PortBlog.API.Controllers.AdminController.GetContact">
|
||||
<summary>
|
||||
Get Candidate details with social links by candidateid
|
||||
Get contact details (candidate with social links) for the logged-in candidate.
|
||||
</summary>
|
||||
<param name="candidateId">The id of the candidate whose detials to get with social links</param>
|
||||
<returns>Candidate details with sociallinks</returns>
|
||||
<returns>Candidate details with social links</returns>
|
||||
<response code="200">Returns the requested candidate details with social links</response>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Controllers.AdminController.GetResume(System.Int32)">
|
||||
<member name="M:PortBlog.API.Controllers.AdminController.GetResume">
|
||||
<summary>
|
||||
Get Candidate resume by candidateid
|
||||
Get resume for the logged-in candidate.
|
||||
</summary>
|
||||
<param name="candidateId">The id of the candidate whose resume to get</param>
|
||||
<returns>Candidate resume</returns>
|
||||
<response code="200">Returns the requested candidate resume</response>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Controllers.AdminController.GetProjects(System.Int32)">
|
||||
<member name="M:PortBlog.API.Controllers.AdminController.GetProjects">
|
||||
<summary>
|
||||
Get Candidate projects by candidateid
|
||||
Get projects for the logged-in candidate.
|
||||
</summary>
|
||||
<param name="candidateId">The id of the candidate whose projects to get</param>
|
||||
<returns>Candidate projects</returns>
|
||||
<response code="200">Returns the requested candidate projects</response>
|
||||
</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">
|
||||
<summary>
|
||||
Controller for handling authentication-related operations.
|
||||
@ -401,6 +445,31 @@
|
||||
The project categories of all the projects
|
||||
</summary>
|
||||
</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">
|
||||
<summary>
|
||||
CV details of the candidate
|
||||
@ -421,6 +490,29 @@
|
||||
The work experiences of the candidate
|
||||
</summary>
|
||||
</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">
|
||||
<summary>
|
||||
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>
|
||||
<returns>A task that represents the asynchronous revoke operation.</returns>
|
||||
</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">
|
||||
<summary>
|
||||
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,
|
||||
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<Project, ProjectDto>()
|
||||
|
||||
@ -99,17 +99,19 @@ builder.Services.AddAutoMapper(cfg => cfg.AddMaps(AppDomain.CurrentDomain.GetAss
|
||||
builder.Services.AddCache(builder.Configuration);
|
||||
|
||||
|
||||
// Registering API Versioning Specification services
|
||||
builder.Services.AddApiVersioning(setupAction =>
|
||||
{
|
||||
setupAction.ReportApiVersions = true;
|
||||
setupAction.AssumeDefaultVersionWhenUnspecified = true;
|
||||
setupAction.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
}).AddMvc()
|
||||
.AddApiExplorer(setupAction =>
|
||||
{
|
||||
setupAction.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
builder.Services
|
||||
.AddApiVersioning(options =>
|
||||
{
|
||||
options.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
options.ReportApiVersions = true;
|
||||
})
|
||||
.AddMvc() // IMPORTANT
|
||||
.AddApiExplorer(options =>
|
||||
{
|
||||
options.GroupNameFormat = "'v'V";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
@ -168,6 +170,8 @@ builder.Services.AddSwaggerGen(c =>
|
||||
c.AddSecurityRequirement(requirement);
|
||||
});
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();
|
||||
|
||||
var jwtKey = builder.Configuration["Jwt:Key"];
|
||||
if (string.IsNullOrEmpty(jwtKey))
|
||||
{
|
||||
@ -196,8 +200,6 @@ builder.Services.AddAuthentication(options =>
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseCors();
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler();
|
||||
@ -211,6 +213,12 @@ if (app.Environment.IsDevelopment())
|
||||
{
|
||||
// Get the IApiVersionDescriptionProvider from the app's service provider
|
||||
var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
|
||||
|
||||
foreach (var d in apiVersionDescriptionProvider.ApiVersionDescriptions)
|
||||
{
|
||||
Console.WriteLine($"GroupName: {d.GroupName}");
|
||||
}
|
||||
|
||||
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
|
||||
{
|
||||
setupAction.SwaggerEndpoint(
|
||||
@ -229,12 +237,16 @@ app.UseStaticFiles(new StaticFileOptions()
|
||||
RequestPath = new PathString("/images")
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseCors();
|
||||
|
||||
app.UseMiddleware<ApiKeyMiddleware>();
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseMiddleware<ApiKeyMiddleware>();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
|
||||
@ -15,5 +15,72 @@ namespace PortBlog.API.Repositories.Contracts
|
||||
Task<Resume?> GetProjectsAsync(int candidateId);
|
||||
|
||||
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 PortBlog.API.DbContexts;
|
||||
using PortBlog.API.Entities;
|
||||
using PortBlog.API.Models;
|
||||
using PortBlog.API.Repositories.Contracts;
|
||||
|
||||
namespace PortBlog.API.Repositories
|
||||
{
|
||||
/// <summary>
|
||||
/// Repository for managing resume-related data access and operations.
|
||||
/// </summary>
|
||||
public class ResumeRepository : IResumeRepository
|
||||
{
|
||||
private readonly CvBlogContext _cvBlogContext;
|
||||
@ -14,9 +16,16 @@ namespace PortBlog.API.Repositories
|
||||
{
|
||||
_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)
|
||||
{
|
||||
if(includeOtherData)
|
||||
if (includeOtherData)
|
||||
{
|
||||
return await _cvBlogContext.Resumes
|
||||
.Include(r => r.Candidate)
|
||||
@ -83,5 +92,180 @@ namespace PortBlog.API.Repositories
|
||||
|
||||
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>
|
||||
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.Role, "Admin")
|
||||
};
|
||||
|
||||
if (candidate != null)
|
||||
{
|
||||
claims.Add(new Claim("CandidateId", candidate.CandidateId.ToString()));
|
||||
}
|
||||
|
||||
var jwtKey = configuration["Jwt:Key"];
|
||||
if (string.IsNullOrEmpty(jwtKey))
|
||||
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