dev #4
@ -4,27 +4,19 @@ 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.
|
||||
/// </summary>
|
||||
[Route("api/v{version:apiVersion}/admin")]
|
||||
[ApiController]
|
||||
[ApiVersion(1)]
|
||||
[Authorize]
|
||||
public class AdminController : Controller
|
||||
public class AdminController(ILogger<CvController> logger, ICandidateRepository candidateRepository, IResumeRepository resumeRepository, IMapper mapper) : Controller
|
||||
{
|
||||
private readonly ILogger<CvController> _logger;
|
||||
private readonly ICandidateRepository _candidateRepository;
|
||||
private readonly IResumeRepository _resumeRepository;
|
||||
private readonly IMapper _mapper;
|
||||
public AdminController(ILogger<CvController> logger, ICandidateRepository candidateRepository, IResumeRepository resumeRepository, IMailService mailService, IMapper mapper, IMailRepository mailRepository)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_candidateRepository = candidateRepository ?? throw new ArgumentNullException(nameof(candidateRepository));
|
||||
_resumeRepository = resumeRepository ?? throw new ArgumentNullException(nameof(resumeRepository));
|
||||
_mapper = mapper;
|
||||
}
|
||||
/// <summary>
|
||||
/// Get hobbies of the candidate by candidateid
|
||||
/// </summary>
|
||||
@ -40,20 +32,20 @@ namespace PortBlog.API.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await _candidateRepository.CandidateExistAsync(candidateId))
|
||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
||||
{
|
||||
_logger.LogInformation($"Candidate with id {candidateId} wasn't found when fetching about details.");
|
||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching about details.", candidateId);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var aboutDetails = await _resumeRepository.GetHobbiesAsync(candidateId);
|
||||
var aboutDetails = await resumeRepository.GetHobbiesAsync(candidateId);
|
||||
|
||||
return Ok(_mapper.Map<AboutDto>(aboutDetails));
|
||||
return Ok(mapper.Map<AboutDto>(aboutDetails));
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical($"Exception while getting about details for the candidate with id {candidateId}.", ex);
|
||||
logger.LogCritical(ex, "Exception while getting about details for the candidate with id {CandidateId}.", candidateId);
|
||||
return StatusCode(500, "A problem happened while handling your request.");
|
||||
}
|
||||
}
|
||||
@ -73,20 +65,20 @@ namespace PortBlog.API.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await _candidateRepository.CandidateExistAsync(candidateId))
|
||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
||||
{
|
||||
_logger.LogInformation($"Candidate with id {candidateId} wasn't found when fetching candidate with social links.");
|
||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching candidate with social links.", candidateId);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var contact = await _resumeRepository.GetCandidateWithSocialLinksAsync(candidateId);
|
||||
var contact = await resumeRepository.GetCandidateWithSocialLinksAsync(candidateId);
|
||||
|
||||
return Ok(_mapper.Map<CandidateSocialLinksDto>(contact));
|
||||
return Ok(mapper.Map<CandidateSocialLinksDto>(contact));
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical($"Exception while getting contact for the candidate with social links with id {candidateId}.", ex);
|
||||
logger.LogCritical(ex, "Exception while getting contact for the candidate with social links with id {CandidateId}.", candidateId);
|
||||
return StatusCode(500, "A problem happened while handling your request.");
|
||||
}
|
||||
}
|
||||
@ -106,20 +98,20 @@ namespace PortBlog.API.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await _candidateRepository.CandidateExistAsync(candidateId))
|
||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
||||
{
|
||||
_logger.LogInformation($"Candidate with id {candidateId} wasn't found when fetching resume.");
|
||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching resume.", candidateId);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var resume = await _resumeRepository.GetResumeAsync(candidateId);
|
||||
var resume = await resumeRepository.GetResumeAsync(candidateId);
|
||||
|
||||
return Ok(_mapper.Map<ResumeDto>(resume));
|
||||
return Ok(mapper.Map<ResumeDto>(resume));
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical($"Exception while getting resume for the candidate with id {candidateId}.", ex);
|
||||
logger.LogCritical(ex, "Exception while getting resume for the candidate with id {CandidateId}.", candidateId);
|
||||
return StatusCode(500, "A problem happened while handling your request.");
|
||||
}
|
||||
}
|
||||
@ -139,20 +131,20 @@ namespace PortBlog.API.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await _candidateRepository.CandidateExistAsync(candidateId))
|
||||
if (!await candidateRepository.CandidateExistAsync(candidateId))
|
||||
{
|
||||
_logger.LogInformation($"Candidate with id {candidateId} wasn't found when fetching projects.");
|
||||
logger.LogInformation("Candidate with id {CandidateId} wasn't found when fetching projects.", candidateId);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var projects = await _resumeRepository.GetProjectsAsync(candidateId);
|
||||
var projects = await resumeRepository.GetProjectsAsync(candidateId);
|
||||
|
||||
return Ok(_mapper.Map<ProjectsDto>(projects));
|
||||
return Ok(mapper.Map<ProjectsDto>(projects));
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical($"Exception while getting projects for the candidate with id {candidateId}.", ex);
|
||||
logger.LogCritical(ex, "Exception while getting projects for the candidate with id {CandidateId}.", candidateId);
|
||||
return StatusCode(500, "A problem happened while handling your request.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
using Asp.Versioning;
|
||||
using AutoMapper;
|
||||
using Azure.Core;
|
||||
using KBR.Cache;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PortBlog.API.Entities;
|
||||
using PortBlog.API.Models;
|
||||
using PortBlog.API.Repositories.Contracts;
|
||||
using PortBlog.API.Services;
|
||||
using PortBlog.API.Services.Contracts;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PortBlog.API.Controllers
|
||||
{
|
||||
@ -24,7 +20,7 @@ namespace PortBlog.API.Controllers
|
||||
/// <summary>
|
||||
/// Generates a One-Time Password (OTP) for the specified candidate and sends it via email.
|
||||
/// </summary>
|
||||
/// <param name="candidateId">The ID of the candidate for whom the OTP is generated.</param>
|
||||
/// <param name="email">The email of the candidate for whom the OTP is generated.</param>
|
||||
/// <returns>An ActionResult indicating the result of the operation.</returns>
|
||||
[HttpPost("GenerateOtp")]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
@ -38,7 +34,7 @@ namespace PortBlog.API.Controllers
|
||||
var candidate = await candidateRepository.GetCandidateAsync(email);
|
||||
if (candidate == null)
|
||||
{
|
||||
logger.LogInformation($"Candidate with email ({email}) wasn't found.");
|
||||
logger.LogInformation("Candidate with email ({Email}) wasn't found.", email);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
@ -62,7 +58,7 @@ namespace PortBlog.API.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogCritical($"Exception while sending OTP for {messageSendDto.ToEmail}.", ex);
|
||||
logger.LogCritical(ex, "Exception while sending OTP for {ToEmail}.", messageSendDto.ToEmail);
|
||||
return StatusCode(500, "A problem happened while handling your request.");
|
||||
}
|
||||
}
|
||||
@ -108,11 +104,15 @@ namespace PortBlog.API.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogCritical($"Exception while validating OTP for {request.UserId}.", ex);
|
||||
logger.LogCritical(ex, "Exception while validating OTP for {UserId}.", request?.UserId);
|
||||
return StatusCode(500, "A problem happened while handling your request.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the access token using a valid refresh token from the HttpOnly cookie.
|
||||
/// </summary>
|
||||
/// <returns>An IActionResult containing the new access token if the refresh token is valid; otherwise, an appropriate error response.</returns>
|
||||
[HttpPost("RefreshToken")]
|
||||
public async Task<IActionResult> Refresh()
|
||||
{
|
||||
@ -120,7 +120,7 @@ namespace PortBlog.API.Controllers
|
||||
if (!Request.Cookies.TryGetValue("refreshToken", out var refreshToken))
|
||||
return Unauthorized();
|
||||
|
||||
var matchedToken = await authService.GetRefreshTokenAsync(refreshToken);
|
||||
var matchedToken = await authService.GetRefreshToken(refreshToken);
|
||||
if (matchedToken == null) return Forbid();
|
||||
|
||||
// Rotate refresh token
|
||||
@ -147,6 +147,10 @@ namespace PortBlog.API.Controllers
|
||||
return Ok(new { accessToken = newAccessToken });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs out the current user by removing the refresh token cookie and invalidating the refresh token.
|
||||
/// </summary>
|
||||
/// <returns>An IActionResult indicating the result of the logout operation.</returns>
|
||||
[HttpPost("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
|
||||
@ -4,6 +4,16 @@
|
||||
<name>PortBlog.API</name>
|
||||
</assembly>
|
||||
<members>
|
||||
<member name="T:PortBlog.API.Controllers.AdminController">
|
||||
<summary>
|
||||
Controller for administrative actions related to candidates and their resumes.
|
||||
</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)">
|
||||
<summary>
|
||||
Controller for administrative actions related to candidates and their resumes.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Controllers.AdminController.GetHobbies(System.Int32)">
|
||||
<summary>
|
||||
Get hobbies of the candidate by candidateid
|
||||
@ -50,7 +60,7 @@
|
||||
<summary>
|
||||
Generates a One-Time Password (OTP) for the specified candidate and sends it via email.
|
||||
</summary>
|
||||
<param name="candidateId">The ID of the candidate for whom the OTP is generated.</param>
|
||||
<param name="email">The email of the candidate for whom the OTP is generated.</param>
|
||||
<returns>An ActionResult indicating the result of the operation.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Controllers.AuthController.VerifyOtp(PortBlog.API.Models.VerifyOtpRequest)">
|
||||
@ -60,6 +70,18 @@
|
||||
<param name="request">The request containing the user ID and OTP code to verify.</param>
|
||||
<returns>An ActionResult indicating the result of the verification.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Controllers.AuthController.Refresh">
|
||||
<summary>
|
||||
Refreshes the access token using a valid refresh token from the HttpOnly cookie.
|
||||
</summary>
|
||||
<returns>An IActionResult containing the new access token if the refresh token is valid; otherwise, an appropriate error response.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Controllers.AuthController.Logout">
|
||||
<summary>
|
||||
Logs out the current user by removing the refresh token cookie and invalidating the refresh token.
|
||||
</summary>
|
||||
<returns>An IActionResult indicating the result of the logout operation.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Controllers.CvController.Get(System.Int32)">
|
||||
<summary>
|
||||
Get CV details of the candidate by candidateid.
|
||||
@ -399,6 +421,24 @@
|
||||
The work experiences of the candidate
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:PortBlog.API.Services.AuthService">
|
||||
<summary>
|
||||
Provides authentication services such as OTP generation, validation, and JWT token management.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.AuthService.#ctor(Microsoft.Extensions.Configuration.IConfiguration,KBR.Cache.IAppDistributedCache,PortBlog.API.DbContexts.CvBlogContext)">
|
||||
<summary>
|
||||
Provides authentication services such as OTP generation, validation, and JWT token management.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.AuthService.GenerateOtp(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Generates a one-time password (OTP) for the specified email address and stores the secret key in the cache.
|
||||
</summary>
|
||||
<param name="email">The email address for which to generate the OTP.</param>
|
||||
<param name="ct">A cancellation token that can be used to cancel the operation.</param>
|
||||
<returns>A task that represents the asynchronous operation. The task result contains the generated OTP as a string.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.AuthService.ValidateOtp(System.String,System.String)">
|
||||
<summary>
|
||||
Validates the provided OTP against the secret key.
|
||||
@ -407,5 +447,157 @@
|
||||
<param name="otp">The OTP to validate.</param>
|
||||
<returns>True if the OTP is valid; otherwise, false.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.AuthService.GenerateAccessToken(System.String)">
|
||||
<summary>
|
||||
Generates a JWT access token for the specified username.
|
||||
</summary>
|
||||
<param name="username">The username for which to generate the access token.</param>
|
||||
<returns>The generated JWT access token as a string.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.AuthService.GenerateRefreshToken">
|
||||
<summary>
|
||||
Generates a secure random refresh token as a Base64-encoded string.
|
||||
</summary>
|
||||
<returns>The generated refresh token.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.AuthService.SaveRefreshToken(System.String,System.String,Microsoft.AspNetCore.Http.HttpContext)">
|
||||
<summary>
|
||||
Saves a refresh token for the specified user email and associates it with the current HTTP context.
|
||||
</summary>
|
||||
<param name="userEmail">The email address of the user.</param>
|
||||
<param name="refreshToken">The refresh token to be saved.</param>
|
||||
<param name="httpContext">The current HTTP context containing request information.</param>
|
||||
<returns>A task that represents the asynchronous save operation.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.AuthService.SaveRefreshToken(PortBlog.API.Entities.RefreshToken)">
|
||||
<summary>
|
||||
Saves a refresh token entity to the database.
|
||||
</summary>
|
||||
<param name="refreshToken">The refresh token entity to save.</param>
|
||||
<returns>A task that represents the asynchronous save operation.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.AuthService.GetRefreshToken(System.String)">
|
||||
<summary>
|
||||
Retrieves a valid, non-revoked, and non-expired refresh token entity matching the provided refresh token string.
|
||||
</summary>
|
||||
<param name="refreshToken">The refresh token string to validate and retrieve.</param>
|
||||
<returns>
|
||||
A task that represents the asynchronous operation. The task result contains the matching <see cref="T:PortBlog.API.Entities.RefreshToken"/> entity if found and valid; otherwise, throws <see cref="T:System.InvalidOperationException"/>.
|
||||
</returns>
|
||||
<exception cref="T:System.InvalidOperationException">Thrown if no valid refresh token is found.</exception>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.AuthService.RemoveRefreshToken(System.String)">
|
||||
<summary>
|
||||
Revokes (removes) a refresh token by marking it as revoked in the database if it is valid and not expired.
|
||||
</summary>
|
||||
<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.IAuthService">
|
||||
<summary>
|
||||
Provides authentication-related services such as OTP generation, token management, and refresh token handling.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.Contracts.IAuthService.GenerateOtp(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Generates a one-time password (OTP) for the specified email.
|
||||
</summary>
|
||||
<param name="email">The email address to generate the OTP for.</param>
|
||||
<param name="ct">A cancellation token.</param>
|
||||
<returns>The generated OTP as a string.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.Contracts.IAuthService.ValidateOtp(System.String,System.String)">
|
||||
<summary>
|
||||
Validates the provided OTP against the secret key.
|
||||
</summary>
|
||||
<param name="secretKey">The secret key used for validation.</param>
|
||||
<param name="otp">The OTP to validate.</param>
|
||||
<returns>True if the OTP is valid; otherwise, false.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.Contracts.IAuthService.GenerateAccessToken(System.String)">
|
||||
<summary>
|
||||
Generates an access token for the specified username.
|
||||
</summary>
|
||||
<param name="username">The username for which to generate the access token.</param>
|
||||
<returns>The generated access token as a string.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.Contracts.IAuthService.GenerateRefreshToken">
|
||||
<summary>
|
||||
Generates a new refresh token.
|
||||
</summary>
|
||||
<returns>The generated refresh token as a string.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.Contracts.IAuthService.SaveRefreshToken(System.String,System.String,Microsoft.AspNetCore.Http.HttpContext)">
|
||||
<summary>
|
||||
Saves the refresh token for the specified user and HTTP context.
|
||||
</summary>
|
||||
<param name="userId">The user ID.</param>
|
||||
<param name="refreshToken">The refresh token to save.</param>
|
||||
<param name="httpContext">The HTTP context.</param>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.Contracts.IAuthService.SaveRefreshToken(PortBlog.API.Entities.RefreshToken)">
|
||||
<summary>
|
||||
Saves the specified refresh token.
|
||||
</summary>
|
||||
<param name="refreshToken">The refresh token entity to save.</param>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.Contracts.IAuthService.GetRefreshToken(System.String)">
|
||||
<summary>
|
||||
Retrieves the refresh token entity for the specified token string.
|
||||
</summary>
|
||||
<param name="refreshToken">The refresh token string.</param>
|
||||
<returns>The corresponding <see cref="T:PortBlog.API.Entities.RefreshToken"/> entity.</returns>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.Contracts.IAuthService.RemoveRefreshToken(System.String)">
|
||||
<summary>
|
||||
Removes the specified refresh token.
|
||||
</summary>
|
||||
<param name="refreshToken">The refresh token to remove.</param>
|
||||
</member>
|
||||
<member name="T:PortBlog.API.Services.MailService">
|
||||
<summary>
|
||||
Provides functionality for sending emails and logging message activity.
|
||||
</summary>
|
||||
<remarks>
|
||||
Initializes a new instance of the <see cref="T:PortBlog.API.Services.MailService"/> class.
|
||||
</remarks>
|
||||
<param name="configuration">The application configuration.</param>
|
||||
<param name="logger">The logger instance.</param>
|
||||
<param name="mailRepository">The mail repository for storing messages.</param>
|
||||
<param name="mapper">The AutoMapper instance.</param>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.MailService.#ctor(Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger{PortBlog.API.Services.MailService},PortBlog.API.Repositories.Contracts.IMailRepository,AutoMapper.IMapper)">
|
||||
<summary>
|
||||
Provides functionality for sending emails and logging message activity.
|
||||
</summary>
|
||||
<remarks>
|
||||
Initializes a new instance of the <see cref="T:PortBlog.API.Services.MailService"/> class.
|
||||
</remarks>
|
||||
<param name="configuration">The application configuration.</param>
|
||||
<param name="logger">The logger instance.</param>
|
||||
<param name="mailRepository">The mail repository for storing messages.</param>
|
||||
<param name="mapper">The AutoMapper instance.</param>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.MailService.SendAsync(PortBlog.API.Models.MessageSendDto)">
|
||||
<summary>
|
||||
Sends an email message asynchronously using the provided message details.
|
||||
</summary>
|
||||
<param name="messageSendDto">The message details to send.</param>
|
||||
<returns>A task representing the asynchronous operation.</returns>
|
||||
</member>
|
||||
<member name="T:PortBlog.API.Services.TemplateService">
|
||||
<summary>
|
||||
Provides functionality to render Razor view templates with a specified model.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:PortBlog.API.Services.TemplateService.GetViewTemplate``1(System.String,``0)">
|
||||
<summary>
|
||||
Renders a Razor view template at the specified path using the provided model.
|
||||
</summary>
|
||||
<typeparam name="T">The type of the model to pass to the view.</typeparam>
|
||||
<param name="viewPath">The path to the Razor view template.</param>
|
||||
<param name="model">The model to use when rendering the view.</param>
|
||||
<returns>A task that represents the asynchronous operation. The task result contains the rendered view as a string.</returns>
|
||||
</member>
|
||||
</members>
|
||||
</doc>
|
||||
|
||||
@ -43,7 +43,7 @@ if (!string.IsNullOrEmpty(urls.Value))
|
||||
}
|
||||
|
||||
var allowedCorsOrigins = builder.Configuration.GetSection("AllowedCorsOrigins");
|
||||
string[] origins = Array.Empty<string>();
|
||||
string[] origins = [];
|
||||
|
||||
if (!String.IsNullOrEmpty(allowedCorsOrigins.Value))
|
||||
{
|
||||
@ -78,16 +78,16 @@ builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("PortBlogDBConnectionString");
|
||||
|
||||
if (builder.Configuration.GetValue<bool>("ConnectionStrings:Encryption"))
|
||||
{
|
||||
connectionString = builder.Configuration.DecryptConnectionString(connectionString);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
throw new Exception("Connection string cannot be empty");
|
||||
}
|
||||
|
||||
if (builder.Configuration.GetValue<bool>("ConnectionStrings:Encryption"))
|
||||
{
|
||||
connectionString = builder.Configuration.DecryptConnectionString(connectionString);
|
||||
}
|
||||
|
||||
builder.Services
|
||||
.AddDbContext<CvBlogContext>(dbContextOptions
|
||||
=> dbContextOptions.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
|
||||
@ -111,23 +111,8 @@ builder.Services.AddApiVersioning(setupAction =>
|
||||
setupAction.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
|
||||
var apiVersionDescriptionProvider = builder.Services.BuildServiceProvider()
|
||||
.GetRequiredService<IApiVersionDescriptionProvider>();
|
||||
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
|
||||
{
|
||||
c.SwaggerDoc(
|
||||
$"{description.GroupName}",
|
||||
new()
|
||||
{
|
||||
Title = "Portfolio Blog API",
|
||||
Version = description.ApiVersion.ToString(),
|
||||
Description = "Through this API you can access candidate cv details and along with other details."
|
||||
});
|
||||
}
|
||||
|
||||
// XML Comments file for API Documentation
|
||||
var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
var xmlCommentsFullPath = $"{Path.Combine(AppContext.BaseDirectory, xmlCommentsFile)}";
|
||||
@ -174,16 +159,21 @@ builder.Services.AddSwaggerGen(c =>
|
||||
}
|
||||
};
|
||||
|
||||
var requirement = new OpenApiSecurityRequirement()
|
||||
var requirement = new OpenApiSecurityRequirement
|
||||
{
|
||||
{key, new List<string> {} },
|
||||
{bearerScheme, new List<string>{} }
|
||||
{ key, new List<string>() },
|
||||
{ bearerScheme, new List<string>() }
|
||||
};
|
||||
|
||||
c.AddSecurityRequirement(requirement);
|
||||
});
|
||||
|
||||
var key = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]);
|
||||
var jwtKey = builder.Configuration["Jwt:Key"];
|
||||
if (string.IsNullOrEmpty(jwtKey))
|
||||
{
|
||||
throw new Exception("JWT key cannot be null or empty. Please check your configuration.");
|
||||
}
|
||||
var key = Encoding.UTF8.GetBytes(jwtKey);
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
@ -219,8 +209,9 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(setupAction =>
|
||||
{
|
||||
var descriptions = app.DescribeApiVersions();
|
||||
foreach (var description in descriptions)
|
||||
// Get the IApiVersionDescriptionProvider from the app's service provider
|
||||
var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
|
||||
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
|
||||
{
|
||||
setupAction.SwaggerEndpoint(
|
||||
$"/swagger/{description.GroupName}/swagger.json",
|
||||
|
||||
@ -12,19 +12,17 @@ using System.Text;
|
||||
|
||||
namespace PortBlog.API.Services
|
||||
{
|
||||
public class AuthService : IAuthService
|
||||
/// <summary>
|
||||
/// Provides authentication services such as OTP generation, validation, and JWT token management.
|
||||
/// </summary>
|
||||
public class AuthService(IConfiguration configuration, IAppDistributedCache cache, CvBlogContext context) : IAuthService
|
||||
{
|
||||
private readonly IConfiguration configuration;
|
||||
private readonly IAppDistributedCache cache;
|
||||
private readonly CvBlogContext _context;
|
||||
|
||||
public AuthService(IConfiguration configuration, IAppDistributedCache cache, CvBlogContext context)
|
||||
{
|
||||
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
this._context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a one-time password (OTP) for the specified email address and stores the secret key in the cache.
|
||||
/// </summary>
|
||||
/// <param name="email">The email address for which to generate the OTP.</param>
|
||||
/// <param name="ct">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the generated OTP as a string.</returns>
|
||||
public async Task<string> GenerateOtp(string email, CancellationToken ct = default)
|
||||
{
|
||||
var secretKey = KeyGeneration.GenerateRandomKey(20);
|
||||
@ -51,6 +49,11 @@ namespace PortBlog.API.Services
|
||||
return totp.VerifyTotp(DateTime.UtcNow, otp, out long _, VerificationWindow.RfcSpecifiedNetworkDelay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a JWT access token for the specified username.
|
||||
/// </summary>
|
||||
/// <param name="username">The username for which to generate the access token.</param>
|
||||
/// <returns>The generated JWT access token as a string.</returns>
|
||||
public string GenerateAccessToken(string username)
|
||||
{
|
||||
var claims = new[]
|
||||
@ -80,19 +83,29 @@ namespace PortBlog.API.Services
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a secure random refresh token as a Base64-encoded string.
|
||||
/// </summary>
|
||||
/// <returns>The generated refresh token.</returns>
|
||||
public string GenerateRefreshToken()
|
||||
{
|
||||
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a refresh token for the specified user email and associates it with the current HTTP context.
|
||||
/// </summary>
|
||||
/// <param name="userEmail">The email address of the user.</param>
|
||||
/// <param name="refreshToken">The refresh token to be saved.</param>
|
||||
/// <param name="httpContext">The current HTTP context containing request information.</param>
|
||||
/// <returns>A task that represents the asynchronous save operation.</returns>
|
||||
public async Task SaveRefreshToken(string userEmail, string refreshToken, HttpContext httpContext)
|
||||
{
|
||||
var user = await GetUser(userEmail);
|
||||
if (user == null) throw new InvalidOperationException("User not found.");
|
||||
var user = await GetUser(userEmail) ?? throw new InvalidOperationException("User not found.");
|
||||
|
||||
// Store hashed refresh token in DB
|
||||
var hashedToken = BCrypt.Net.BCrypt.HashPassword(refreshToken);
|
||||
_context.RefreshTokens.Add(new RefreshToken
|
||||
context.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
UserId = user.UserId,
|
||||
Token = hashedToken,
|
||||
@ -101,29 +114,48 @@ namespace PortBlog.API.Services
|
||||
DeviceInfo = httpContext.Request.Headers.UserAgent.ToString()
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a refresh token entity to the database.
|
||||
/// </summary>
|
||||
/// <param name="refreshToken">The refresh token entity to save.</param>
|
||||
/// <returns>A task that represents the asynchronous save operation.</returns>
|
||||
public async Task SaveRefreshToken(RefreshToken refreshToken)
|
||||
{
|
||||
_context.RefreshTokens.Add(refreshToken);
|
||||
await _context.SaveChangesAsync();
|
||||
context.RefreshTokens.Add(refreshToken);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<RefreshToken?> GetRefreshTokenAsync(string refreshToken)
|
||||
/// <summary>
|
||||
/// Retrieves a valid, non-revoked, and non-expired refresh token entity matching the provided refresh token string.
|
||||
/// </summary>
|
||||
/// <param name="refreshToken">The refresh token string to validate and retrieve.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous operation. The task result contains the matching <see cref="RefreshToken"/> entity if found and valid; otherwise, throws <see cref="InvalidOperationException"/>.
|
||||
/// </returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if no valid refresh token is found.</exception>
|
||||
public async Task<RefreshToken> GetRefreshToken(string refreshToken)
|
||||
{
|
||||
// Find user and validate refresh token
|
||||
var refreshEntity = await _context.RefreshTokens
|
||||
var refreshEntity = await context.RefreshTokens
|
||||
.Include(rt => rt.User)
|
||||
.Where(rt => !rt.Revoked && rt.ExpiryDate > DateTime.UtcNow)
|
||||
.ToListAsync();
|
||||
|
||||
return refreshEntity.FirstOrDefault(rt => BCrypt.Net.BCrypt.Verify(refreshToken, rt.Token));
|
||||
var token = refreshEntity.FirstOrDefault(rt => BCrypt.Net.BCrypt.Verify(refreshToken, rt.Token)) ?? throw new InvalidOperationException("Refresh token not found or invalid.");
|
||||
return token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revokes (removes) a refresh token by marking it as revoked in the database if it is valid and not expired.
|
||||
/// </summary>
|
||||
/// <param name="refreshToken">The refresh token to revoke.</param>
|
||||
/// <returns>A task that represents the asynchronous revoke operation.</returns>
|
||||
public async Task RemoveRefreshToken(string refreshToken)
|
||||
{
|
||||
var tokens = await _context.RefreshTokens
|
||||
var tokens = await context.RefreshTokens
|
||||
.Where(rt => rt.Revoked == false && rt.ExpiryDate > DateTime.UtcNow)
|
||||
.ToListAsync();
|
||||
|
||||
@ -133,13 +165,13 @@ namespace PortBlog.API.Services
|
||||
if (refreshTokenEntity != null)
|
||||
{
|
||||
refreshTokenEntity.Revoked = true;
|
||||
await _context.SaveChangesAsync();
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<User?> GetUser(string userEmail)
|
||||
{
|
||||
return await _context.Users.FirstOrDefaultAsync(u => u.Email == userEmail);
|
||||
return await context.Users.FirstOrDefaultAsync(u => u.Email == userEmail);
|
||||
}
|
||||
|
||||
private string GenerateOtp(string secretKey)
|
||||
|
||||
@ -2,22 +2,65 @@
|
||||
|
||||
namespace PortBlog.API.Services.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides authentication-related services such as OTP generation, token management, and refresh token handling.
|
||||
/// </summary>
|
||||
public interface IAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a one-time password (OTP) for the specified email.
|
||||
/// </summary>
|
||||
/// <param name="email">The email address to generate the OTP for.</param>
|
||||
/// <param name="ct">A cancellation token.</param>
|
||||
/// <returns>The generated OTP as a string.</returns>
|
||||
Task<string> GenerateOtp(string email, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the provided OTP against the secret key.
|
||||
/// </summary>
|
||||
/// <param name="secretKey">The secret key used for validation.</param>
|
||||
/// <param name="otp">The OTP to validate.</param>
|
||||
/// <returns>True if the OTP is valid; otherwise, false.</returns>
|
||||
bool ValidateOtp(string secretKey, string otp);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an access token for the specified username.
|
||||
/// </summary>
|
||||
/// <param name="username">The username for which to generate the access token.</param>
|
||||
/// <returns>The generated access token as a string.</returns>
|
||||
string GenerateAccessToken(string username);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new refresh token.
|
||||
/// </summary>
|
||||
/// <returns>The generated refresh token as a string.</returns>
|
||||
string GenerateRefreshToken();
|
||||
|
||||
/// <summary>
|
||||
/// Saves the refresh token for the specified user and HTTP context.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user ID.</param>
|
||||
/// <param name="refreshToken">The refresh token to save.</param>
|
||||
/// <param name="httpContext">The HTTP context.</param>
|
||||
Task SaveRefreshToken(string userId, string refreshToken, HttpContext httpContext);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the specified refresh token.
|
||||
/// </summary>
|
||||
/// <param name="refreshToken">The refresh token entity to save.</param>
|
||||
Task SaveRefreshToken(RefreshToken refreshToken);
|
||||
|
||||
Task<RefreshToken> GetRefreshTokenAsync(string refreshToken);
|
||||
/// <summary>
|
||||
/// Retrieves the refresh token entity for the specified token string.
|
||||
/// </summary>
|
||||
/// <param name="refreshToken">The refresh token string.</param>
|
||||
/// <returns>The corresponding <see cref="RefreshToken"/> entity.</returns>
|
||||
Task<RefreshToken> GetRefreshToken(string refreshToken);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified refresh token.
|
||||
/// </summary>
|
||||
/// <param name="refreshToken">The refresh token to remove.</param>
|
||||
Task RemoveRefreshToken(string refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
namespace PortBlog.API.Services.Contracts
|
||||
{
|
||||
public interface IOtpService
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace PortBlog.API.Services.Contracts
|
||||
{
|
||||
public interface ITokenService
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace PortBlog.API.Services.Contracts
|
||||
{
|
||||
public interface JwtServicecs
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace PortBlog.API.Services
|
||||
{
|
||||
public class JwtService
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -10,29 +10,32 @@ using System.Net.Mail;
|
||||
|
||||
namespace PortBlog.API.Services
|
||||
{
|
||||
public class MailService : IMailService
|
||||
/// <summary>
|
||||
/// Provides functionality for sending emails and logging message activity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="MailService"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="configuration">The application configuration.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
/// <param name="mailRepository">The mail repository for storing messages.</param>
|
||||
/// <param name="mapper">The AutoMapper instance.</param>
|
||||
public class MailService(IConfiguration configuration, ILogger<MailService> logger, IMailRepository mailRepository, IMapper mapper) : IMailService
|
||||
{
|
||||
private readonly ILogger<MailService> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IMailRepository _mailRepository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public MailService(IConfiguration configuration, ILogger<MailService> logger, IMailRepository mailRepository, IMapper mapper)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_mailRepository = mailRepository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an email message asynchronously using the provided message details.
|
||||
/// </summary>
|
||||
/// <param name="messageSendDto">The message details to send.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task SendAsync(MessageSendDto messageSendDto)
|
||||
{
|
||||
_logger.LogInformation($"Sending message from {messageSendDto.Name} ({messageSendDto.FromEmail}).");
|
||||
logger.LogInformation("Sending message from {Name} ({FromEmail}).", messageSendDto.Name, messageSendDto.FromEmail);
|
||||
messageSendDto.Subject = $"Message from {messageSendDto.Name}: Portfolio";
|
||||
var messageEntity = _mapper.Map<Message>(messageSendDto);
|
||||
var messageEntity = mapper.Map<Message>(messageSendDto);
|
||||
try
|
||||
{
|
||||
var mailSettings = _configuration.GetSection("MailSettings").Get<MailSettingsDto>();
|
||||
var mailSettings = configuration.GetSection("MailSettings").Get<MailSettingsDto>();
|
||||
|
||||
if (mailSettings != null && mailSettings.Enable)
|
||||
{
|
||||
@ -45,11 +48,10 @@ namespace PortBlog.API.Services
|
||||
client.EnableSsl = true;
|
||||
client.Credentials = new NetworkCredential(mailSettings.Email, mailSettings.Password);
|
||||
|
||||
using (var mailMessage = new MailMessage(
|
||||
using var mailMessage = new MailMessage(
|
||||
from: new MailAddress(mailSettings.Email),
|
||||
to: new MailAddress(MailHelpers.ReplaceEmailDomain(messageSendDto.ToEmail, mailSettings.Domain), messageSendDto.Name)
|
||||
))
|
||||
{
|
||||
);
|
||||
|
||||
mailMessage.Subject = messageSendDto.Subject;
|
||||
mailMessage.Body = messageSendDto.Content;
|
||||
@ -58,18 +60,22 @@ namespace PortBlog.API.Services
|
||||
client.Send(mailMessage);
|
||||
messageSendDto.SentStatus = (int)MailConstants.MailStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
_mailRepository.AddMessage(messageEntity);
|
||||
await _mailRepository.SaveChangesAsync();
|
||||
mailRepository.AddMessage(messageEntity);
|
||||
await mailRepository.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical($"Exception while sending mail from {new MailAddress(messageSendDto.FromEmail, messageSendDto.Name)}", ex);
|
||||
logger.LogCritical(
|
||||
"Exception while sending mail from {FromEmail} ({Name}): {Exception}",
|
||||
messageSendDto.FromEmail,
|
||||
messageSendDto.Name,
|
||||
ex.Message
|
||||
);
|
||||
messageSendDto.SentStatus = (int)MailConstants.MailStatus.Failed;
|
||||
_mailRepository.AddMessage(messageEntity);
|
||||
await _mailRepository.SaveChangesAsync();
|
||||
mailRepository.AddMessage(messageEntity);
|
||||
await mailRepository.SaveChangesAsync();
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
namespace PortBlog.API.Services
|
||||
{
|
||||
public class OtpService
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -3,8 +3,18 @@ using Razor.Templating.Core;
|
||||
|
||||
namespace PortBlog.API.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides functionality to render Razor view templates with a specified model.
|
||||
/// </summary>
|
||||
public class TemplateService : ITemplateService
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders a Razor view template at the specified path using the provided model.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the model to pass to the view.</typeparam>
|
||||
/// <param name="viewPath">The path to the Razor view template.</param>
|
||||
/// <param name="model">The model to use when rendering the view.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the rendered view as a string.</returns>
|
||||
public async Task<string> GetViewTemplate<T>(string viewPath, T model)
|
||||
{
|
||||
return await RazorTemplateEngine.RenderAsync(viewPath, model);
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
namespace PortBlog.API.Services
|
||||
{
|
||||
public class TokenService
|
||||
{
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user