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 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.");
}
}

View File

@ -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()
{

View File

@ -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>

View File

@ -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",

View File

@ -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)

View File

@ -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);
}
}

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
{
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,31 +48,34 @@ 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;
mailMessage.IsBodyHtml = true;
mailMessage.Subject = messageSendDto.Subject;
mailMessage.Body = messageSendDto.Content;
mailMessage.IsBodyHtml = true;
client.Send(mailMessage);
messageSendDto.SentStatus = (int)MailConstants.MailStatus.Success;
}
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();
}
}

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
{
/// <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);

View File

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