Admin API refactor: claim-based resume CRUD & versioning
Major refactor of AdminController and related services to support full CRUD for candidate resume, projects, hobbies, skills, academics, experiences, certifications, and contact info, all using access token claims for candidate identity. Introduced AdminService and expanded IResumeRepository for granular entity management. Updated DTOs for upsert operations and date support. Improved API versioning (Asp.Versioning.Mvc), Swagger integration, and middleware setup. Added unit test project. Enhanced error handling, documentation, and mapping.
This commit is contained in:
parent
cd01c11be7
commit
5b7952877e
@ -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