dev #4

Merged
rajukottedi merged 7 commits from dev into prod 2026-02-16 10:28:59 +05:30
14 changed files with 395 additions and 161 deletions
Showing only changes of commit cd01c11be7 - Show all commits

View File

@ -4,27 +4,19 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using PortBlog.API.Models; using PortBlog.API.Models;
using PortBlog.API.Repositories.Contracts; using PortBlog.API.Repositories.Contracts;
using PortBlog.API.Services.Contracts;
namespace PortBlog.API.Controllers namespace PortBlog.API.Controllers
{ {
/// <summary>
/// Controller for administrative actions related to candidates and their resumes.
/// </summary>
[Route("api/v{version:apiVersion}/admin")] [Route("api/v{version:apiVersion}/admin")]
[ApiController] [ApiController]
[ApiVersion(1)] [ApiVersion(1)]
[Authorize] [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> /// <summary>
/// Get hobbies of the candidate by candidateid /// Get hobbies of the candidate by candidateid
/// </summary> /// </summary>
@ -40,20 +32,20 @@ namespace PortBlog.API.Controllers
{ {
try 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(); 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) 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."); return StatusCode(500, "A problem happened while handling your request.");
} }
} }
@ -73,20 +65,20 @@ namespace PortBlog.API.Controllers
{ {
try 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(); 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) 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."); return StatusCode(500, "A problem happened while handling your request.");
} }
} }
@ -106,20 +98,20 @@ namespace PortBlog.API.Controllers
{ {
try 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(); 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) 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."); return StatusCode(500, "A problem happened while handling your request.");
} }
} }
@ -139,20 +131,20 @@ namespace PortBlog.API.Controllers
{ {
try 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(); 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) 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."); return StatusCode(500, "A problem happened while handling your request.");
} }
} }

View File

@ -1,15 +1,11 @@
using Asp.Versioning; using Asp.Versioning;
using AutoMapper; using AutoMapper;
using Azure.Core;
using KBR.Cache; using KBR.Cache;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PortBlog.API.Entities; using PortBlog.API.Entities;
using PortBlog.API.Models; using PortBlog.API.Models;
using PortBlog.API.Repositories.Contracts; using PortBlog.API.Repositories.Contracts;
using PortBlog.API.Services;
using PortBlog.API.Services.Contracts; using PortBlog.API.Services.Contracts;
using System.Threading.Tasks;
namespace PortBlog.API.Controllers namespace PortBlog.API.Controllers
{ {
@ -24,7 +20,7 @@ namespace PortBlog.API.Controllers
/// <summary> /// <summary>
/// Generates a One-Time Password (OTP) for the specified candidate and sends it via email. /// Generates a One-Time Password (OTP) for the specified candidate and sends it via email.
/// </summary> /// </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> /// <returns>An ActionResult indicating the result of the operation.</returns>
[HttpPost("GenerateOtp")] [HttpPost("GenerateOtp")]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
@ -38,7 +34,7 @@ namespace PortBlog.API.Controllers
var candidate = await candidateRepository.GetCandidateAsync(email); var candidate = await candidateRepository.GetCandidateAsync(email);
if (candidate == null) if (candidate == null)
{ {
logger.LogInformation($"Candidate with email ({email}) wasn't found."); logger.LogInformation("Candidate with email ({Email}) wasn't found.", email);
return NotFound(); return NotFound();
} }
@ -62,7 +58,7 @@ namespace PortBlog.API.Controllers
} }
catch (Exception ex) 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."); return StatusCode(500, "A problem happened while handling your request.");
} }
} }
@ -108,11 +104,15 @@ namespace PortBlog.API.Controllers
} }
catch (Exception ex) 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."); 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")] [HttpPost("RefreshToken")]
public async Task<IActionResult> Refresh() public async Task<IActionResult> Refresh()
{ {
@ -120,7 +120,7 @@ namespace PortBlog.API.Controllers
if (!Request.Cookies.TryGetValue("refreshToken", out var refreshToken)) if (!Request.Cookies.TryGetValue("refreshToken", out var refreshToken))
return Unauthorized(); return Unauthorized();
var matchedToken = await authService.GetRefreshTokenAsync(refreshToken); var matchedToken = await authService.GetRefreshToken(refreshToken);
if (matchedToken == null) return Forbid(); if (matchedToken == null) return Forbid();
// Rotate refresh token // Rotate refresh token
@ -147,6 +147,10 @@ namespace PortBlog.API.Controllers
return Ok(new { accessToken = newAccessToken }); 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")] [HttpPost("logout")]
public async Task<IActionResult> Logout() public async Task<IActionResult> Logout()
{ {

View File

@ -4,6 +4,16 @@
<name>PortBlog.API</name> <name>PortBlog.API</name>
</assembly> </assembly>
<members> <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)"> <member name="M:PortBlog.API.Controllers.AdminController.GetHobbies(System.Int32)">
<summary> <summary>
Get hobbies of the candidate by candidateid Get hobbies of the candidate by candidateid
@ -50,7 +60,7 @@
<summary> <summary>
Generates a One-Time Password (OTP) for the specified candidate and sends it via email. Generates a One-Time Password (OTP) for the specified candidate and sends it via email.
</summary> </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> <returns>An ActionResult indicating the result of the operation.</returns>
</member> </member>
<member name="M:PortBlog.API.Controllers.AuthController.VerifyOtp(PortBlog.API.Models.VerifyOtpRequest)"> <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> <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> <returns>An ActionResult indicating the result of the verification.</returns>
</member> </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)"> <member name="M:PortBlog.API.Controllers.CvController.Get(System.Int32)">
<summary> <summary>
Get CV details of the candidate by candidateid. Get CV details of the candidate by candidateid.
@ -399,6 +421,24 @@
The work experiences of the candidate The work experiences of the candidate
</summary> </summary>
</member> </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)"> <member name="M:PortBlog.API.Services.AuthService.ValidateOtp(System.String,System.String)">
<summary> <summary>
Validates the provided OTP against the secret key. Validates the provided OTP against the secret key.
@ -407,5 +447,157 @@
<param name="otp">The OTP to validate.</param> <param name="otp">The OTP to validate.</param>
<returns>True if the OTP is valid; otherwise, false.</returns> <returns>True if the OTP is valid; otherwise, false.</returns>
</member> </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> </members>
</doc> </doc>

View File

@ -43,7 +43,7 @@ if (!string.IsNullOrEmpty(urls.Value))
} }
var allowedCorsOrigins = builder.Configuration.GetSection("AllowedCorsOrigins"); var allowedCorsOrigins = builder.Configuration.GetSection("AllowedCorsOrigins");
string[] origins = Array.Empty<string>(); string[] origins = [];
if (!String.IsNullOrEmpty(allowedCorsOrigins.Value)) if (!String.IsNullOrEmpty(allowedCorsOrigins.Value))
{ {
@ -78,16 +78,16 @@ builder.Services.AddEndpointsApiExplorer();
var connectionString = builder.Configuration.GetConnectionString("PortBlogDBConnectionString"); var connectionString = builder.Configuration.GetConnectionString("PortBlogDBConnectionString");
if (builder.Configuration.GetValue<bool>("ConnectionStrings:Encryption"))
{
connectionString = builder.Configuration.DecryptConnectionString(connectionString);
}
if (string.IsNullOrEmpty(connectionString)) if (string.IsNullOrEmpty(connectionString))
{ {
throw new Exception("Connection string cannot be empty"); throw new Exception("Connection string cannot be empty");
} }
if (builder.Configuration.GetValue<bool>("ConnectionStrings:Encryption"))
{
connectionString = builder.Configuration.DecryptConnectionString(connectionString);
}
builder.Services builder.Services
.AddDbContext<CvBlogContext>(dbContextOptions .AddDbContext<CvBlogContext>(dbContextOptions
=> dbContextOptions.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); => dbContextOptions.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
@ -111,23 +111,8 @@ builder.Services.AddApiVersioning(setupAction =>
setupAction.SubstituteApiVersionInUrl = true; setupAction.SubstituteApiVersionInUrl = true;
}); });
var apiVersionDescriptionProvider = builder.Services.BuildServiceProvider()
.GetRequiredService<IApiVersionDescriptionProvider>();
builder.Services.AddSwaggerGen(c => 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 // XML Comments file for API Documentation
var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlCommentsFullPath = $"{Path.Combine(AppContext.BaseDirectory, xmlCommentsFile)}"; 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> {} }, { key, new List<string>() },
{bearerScheme, new List<string>{} } { bearerScheme, new List<string>() }
}; };
c.AddSecurityRequirement(requirement); 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 => builder.Services.AddAuthentication(options =>
{ {
@ -219,8 +209,9 @@ if (app.Environment.IsDevelopment())
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(setupAction => app.UseSwaggerUI(setupAction =>
{ {
var descriptions = app.DescribeApiVersions(); // Get the IApiVersionDescriptionProvider from the app's service provider
foreach (var description in descriptions) var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
{ {
setupAction.SwaggerEndpoint( setupAction.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json", $"/swagger/{description.GroupName}/swagger.json",

View File

@ -12,19 +12,17 @@ using System.Text;
namespace PortBlog.API.Services 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; /// <summary>
private readonly IAppDistributedCache cache; /// Generates a one-time password (OTP) for the specified email address and stores the secret key in the cache.
private readonly CvBlogContext _context; /// </summary>
/// <param name="email">The email address for which to generate the OTP.</param>
public AuthService(IConfiguration configuration, IAppDistributedCache cache, CvBlogContext context) /// <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>
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
this._context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<string> GenerateOtp(string email, CancellationToken ct = default) public async Task<string> GenerateOtp(string email, CancellationToken ct = default)
{ {
var secretKey = KeyGeneration.GenerateRandomKey(20); var secretKey = KeyGeneration.GenerateRandomKey(20);
@ -51,6 +49,11 @@ namespace PortBlog.API.Services
return totp.VerifyTotp(DateTime.UtcNow, otp, out long _, VerificationWindow.RfcSpecifiedNetworkDelay); 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) public string GenerateAccessToken(string username)
{ {
var claims = new[] var claims = new[]
@ -80,19 +83,29 @@ namespace PortBlog.API.Services
return new JwtSecurityTokenHandler().WriteToken(token); 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() public string GenerateRefreshToken()
{ {
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); 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) public async Task SaveRefreshToken(string userEmail, string refreshToken, HttpContext httpContext)
{ {
var user = await GetUser(userEmail); var user = await GetUser(userEmail) ?? throw new InvalidOperationException("User not found.");
if (user == null) throw new InvalidOperationException("User not found.");
// Store hashed refresh token in DB // Store hashed refresh token in DB
var hashedToken = BCrypt.Net.BCrypt.HashPassword(refreshToken); var hashedToken = BCrypt.Net.BCrypt.HashPassword(refreshToken);
_context.RefreshTokens.Add(new RefreshToken context.RefreshTokens.Add(new RefreshToken
{ {
UserId = user.UserId, UserId = user.UserId,
Token = hashedToken, Token = hashedToken,
@ -101,29 +114,48 @@ namespace PortBlog.API.Services
DeviceInfo = httpContext.Request.Headers.UserAgent.ToString() 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) public async Task SaveRefreshToken(RefreshToken refreshToken)
{ {
_context.RefreshTokens.Add(refreshToken); context.RefreshTokens.Add(refreshToken);
await _context.SaveChangesAsync(); 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 // Find user and validate refresh token
var refreshEntity = await _context.RefreshTokens var refreshEntity = await context.RefreshTokens
.Include(rt => rt.User) .Include(rt => rt.User)
.Where(rt => !rt.Revoked && rt.ExpiryDate > DateTime.UtcNow) .Where(rt => !rt.Revoked && rt.ExpiryDate > DateTime.UtcNow)
.ToListAsync(); .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) 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) .Where(rt => rt.Revoked == false && rt.ExpiryDate > DateTime.UtcNow)
.ToListAsync(); .ToListAsync();
@ -133,13 +165,13 @@ namespace PortBlog.API.Services
if (refreshTokenEntity != null) if (refreshTokenEntity != null)
{ {
refreshTokenEntity.Revoked = true; refreshTokenEntity.Revoked = true;
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
} }
private async Task<User?> GetUser(string userEmail) 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) private string GenerateOtp(string secretKey)

View File

@ -2,22 +2,65 @@
namespace PortBlog.API.Services.Contracts namespace PortBlog.API.Services.Contracts
{ {
/// <summary>
/// Provides authentication-related services such as OTP generation, token management, and refresh token handling.
/// </summary>
public interface IAuthService 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); 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); 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); string GenerateAccessToken(string username);
/// <summary>
/// Generates a new refresh token.
/// </summary>
/// <returns>The generated refresh token as a string.</returns>
string GenerateRefreshToken(); 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); 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 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); Task RemoveRefreshToken(string refreshToken);
} }
} }

View File

@ -1,6 +0,0 @@
namespace PortBlog.API.Services.Contracts
{
public interface IOtpService
{
}
}

View File

@ -1,6 +0,0 @@
namespace PortBlog.API.Services.Contracts
{
public interface ITokenService
{
}
}

View File

@ -1,6 +0,0 @@
namespace PortBlog.API.Services.Contracts
{
public interface JwtServicecs
{
}
}

View File

@ -1,6 +0,0 @@
namespace PortBlog.API.Services
{
public class JwtService
{
}
}

View File

@ -10,29 +10,32 @@ using System.Net.Mail;
namespace PortBlog.API.Services 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) 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"; messageSendDto.Subject = $"Message from {messageSendDto.Name}: Portfolio";
var messageEntity = _mapper.Map<Message>(messageSendDto); var messageEntity = mapper.Map<Message>(messageSendDto);
try try
{ {
var mailSettings = _configuration.GetSection("MailSettings").Get<MailSettingsDto>(); var mailSettings = configuration.GetSection("MailSettings").Get<MailSettingsDto>();
if (mailSettings != null && mailSettings.Enable) if (mailSettings != null && mailSettings.Enable)
{ {
@ -45,11 +48,10 @@ namespace PortBlog.API.Services
client.EnableSsl = true; client.EnableSsl = true;
client.Credentials = new NetworkCredential(mailSettings.Email, mailSettings.Password); client.Credentials = new NetworkCredential(mailSettings.Email, mailSettings.Password);
using (var mailMessage = new MailMessage( using var mailMessage = new MailMessage(
from: new MailAddress(mailSettings.Email), from: new MailAddress(mailSettings.Email),
to: new MailAddress(MailHelpers.ReplaceEmailDomain(messageSendDto.ToEmail, mailSettings.Domain), messageSendDto.Name) to: new MailAddress(MailHelpers.ReplaceEmailDomain(messageSendDto.ToEmail, mailSettings.Domain), messageSendDto.Name)
)) );
{
mailMessage.Subject = messageSendDto.Subject; mailMessage.Subject = messageSendDto.Subject;
mailMessage.Body = messageSendDto.Content; mailMessage.Body = messageSendDto.Content;
@ -58,18 +60,22 @@ namespace PortBlog.API.Services
client.Send(mailMessage); client.Send(mailMessage);
messageSendDto.SentStatus = (int)MailConstants.MailStatus.Success; messageSendDto.SentStatus = (int)MailConstants.MailStatus.Success;
} }
}
_mailRepository.AddMessage(messageEntity); mailRepository.AddMessage(messageEntity);
await _mailRepository.SaveChangesAsync(); await mailRepository.SaveChangesAsync();
} }
} }
catch (Exception ex) 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; messageSendDto.SentStatus = (int)MailConstants.MailStatus.Failed;
_mailRepository.AddMessage(messageEntity); mailRepository.AddMessage(messageEntity);
await _mailRepository.SaveChangesAsync(); await mailRepository.SaveChangesAsync();
throw new Exception(); throw new Exception();
} }
} }

View File

@ -1,6 +0,0 @@
namespace PortBlog.API.Services
{
public class OtpService
{
}
}

View File

@ -3,8 +3,18 @@ using Razor.Templating.Core;
namespace PortBlog.API.Services namespace PortBlog.API.Services
{ {
/// <summary>
/// Provides functionality to render Razor view templates with a specified model.
/// </summary>
public class TemplateService : ITemplateService 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) public async Task<string> GetViewTemplate<T>(string viewPath, T model)
{ {
return await RazorTemplateEngine.RenderAsync(viewPath, model); return await RazorTemplateEngine.RenderAsync(viewPath, model);

View File

@ -1,6 +0,0 @@
namespace PortBlog.API.Services
{
public class TokenService
{
}
}