diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs new file mode 100644 index 0000000..14941ce --- /dev/null +++ b/Controllers/AuthController.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Mvc; +using apekade.Enums; +using apekade.Services; +using apekade.Models.Dto.AuthDto; +using apekade.Models.Dto; +using apekade.Models.Validation; + +namespace apekade.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + private readonly IAuthService _authService; + + public AuthController(IAuthService authService) + { + _authService = authService; + } + + [HttpPost("register")] + public async Task Register([FromBody] RegisterDto registerDto) + { + var validator = new RegisterValidator(); + var result = validator.Validate(registerDto); + + if (!result.IsValid) + { + var firstError = result.Errors.Select(e => new { error = e.ErrorMessage }).FirstOrDefault(); + return this.ApiRes(400, false, "Validation error", firstError); + } + + var response = await _authService.Register(registerDto); + if (!response.Status) + { + return this.ApiRes(response.Code, response.Status, response.Message, response.Data); + } + return this.ApiRes(response.Code, response.Status, response.Message, response.Data); + } + + [HttpPost("login")] + public async Task Login([FromBody] LoginDto loginDto) + { + var validator = new LoginValidator(); + var result = validator.Validate(loginDto); + + if (!result.IsValid) + { + var firstError = result.Errors.Select(e => new { error = e.ErrorMessage }).FirstOrDefault(); + return this.ApiRes(400, false, "Validation error", firstError); + } + + var response = await _authService.Login(loginDto); + if (!response.Status) + { + return this.ApiRes(response.Code, response.Status, response.Message, response.Data); + } + return this.ApiRes(response.Code, response.Status, response.Message, response.Data); + } +} \ No newline at end of file diff --git a/Controllers/TestController.cs b/Controllers/TestController.cs index 71f4bb8..aa81c70 100644 --- a/Controllers/TestController.cs +++ b/Controllers/TestController.cs @@ -13,12 +13,7 @@ public class TestController : ControllerBase [HttpGet] public IActionResult GetServerStatus() { - var response = new ApiRes{ - Status = true, - Code= 200, - Data = new { Message = "Server Online" } - }; - return Ok(response); + return this.ApiRes(200, true, "Server Online", new { Msg = "Server Online" }); } // Only SuperAdmin can access this endpoint @@ -26,7 +21,7 @@ public IActionResult GetServerStatus() [HttpGet("superadmin")] public IActionResult SuperAdminOnly() { - return Ok("Only SuperAdmin can access this."); + return this.ApiRes(200, true, "Only SuperAdmin can access this.", new {}); } // Only Seller can access this endpoint @@ -34,7 +29,7 @@ public IActionResult SuperAdminOnly() [HttpGet("seller")] public IActionResult SellerOnly() { - return Ok("Only Sellers can access this."); + return this.ApiRes(200, true, "Only Sellers can access this.", new {}); } // Only Buyer can access this endpoint @@ -42,7 +37,7 @@ public IActionResult SellerOnly() [HttpGet("buyer")] public IActionResult BuyerOnly() { - return Ok("Only Buyers can access this."); + return this.ApiRes(200, true, "Only Buyers can access this.", new {}); } // Both Seller and Buyer can access this endpoint @@ -50,7 +45,7 @@ public IActionResult BuyerOnly() [HttpGet("seller-buyer")] public IActionResult SellerAndBuyerAccess() { - return Ok("Both Sellers and Buyers can access this."); + return this.ApiRes(200, true, "Both Sellers and Buyers can access this.", new {}); } // Any authenticated user can access this endpoint @@ -58,13 +53,13 @@ public IActionResult SellerAndBuyerAccess() [HttpGet("common")] public IActionResult CommonAccess() { - return Ok("Any authenticated user can access this."); + return this.ApiRes(200, true, "Any authenticated user can access this.", new {}); } - + // Any one can access this endpoint [HttpGet("open")] public IActionResult OpenAccess() { - return Ok("Any one can access this."); + return this.ApiRes(200, true, "Anyone can access this.", new {}); } } \ No newline at end of file diff --git a/Controllers/UserController.cs b/Controllers/UserController.cs index 416d9dd..eecfde7 100644 --- a/Controllers/UserController.cs +++ b/Controllers/UserController.cs @@ -1,23 +1,23 @@ using Microsoft.AspNetCore.Mvc; using apekade.Enums; using apekade.Services; -using apekade.Models.Dto.UserDto; +using Microsoft.AspNetCore.Authorization; namespace apekade.Controllers; [ApiController] [Route("api/[controller]")] +[Authorize] public class UserController : ControllerBase{ private readonly IUserService _userService; - public UserController(IUserService userService){ _userService = userService; } - [HttpPost] - public async Task CreateUser([FromBody] UserReqtDto userReqtDto){ + [HttpPost("test")] + public IActionResult CreateUser([FromBody] string email){ - var response = await _userService.CreateNewUser(userReqtDto); + var response = email; return Ok(response); } } \ No newline at end of file diff --git a/Helpers/GenerateJwtToken.cs b/Helpers/GenerateJwtToken.cs deleted file mode 100644 index 44a5b5d..0000000 --- a/Helpers/GenerateJwtToken.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using Microsoft.IdentityModel.Tokens; -using apekade.Models; - -namespace apekade.Helpers; - -public class GenerateJwtToken{ - private readonly IConfiguration _configuration; - - public GenerateJwtToken(IConfiguration configuration) - { - _configuration = configuration; - } - public string GenerateJwt(User user) - { - var tokenHandler = new JwtSecurityTokenHandler(); - - var appSettingToken = _configuration.GetSection("AppSettings:Token").Value; - if (appSettingToken is null) - throw new Exception("AppSettings Token is null!"); - - var key = Encoding.UTF8.GetBytes(appSettingToken); - - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(new[] - { - new Claim(ClaimTypes.NameIdentifier, user.Id), - new Claim(ClaimTypes.Email, user.Email), - new Claim(ClaimTypes.Role, user.Role.ToString()) - }), - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) - }; - - var token = tokenHandler.CreateToken(tokenDescriptor); - return tokenHandler.WriteToken(token); - } -} \ No newline at end of file diff --git a/Helpers/HashPassword.cs b/Helpers/HashPassword.cs index 8b10e9b..3f907c8 100644 --- a/Helpers/HashPassword.cs +++ b/Helpers/HashPassword.cs @@ -1,19 +1,37 @@ using System.Security.Cryptography; - +using Microsoft.AspNetCore.Cryptography.KeyDerivation; namespace apekade.Helpers; -public class HashPassword{ - public static void CreatePasswordHash(string password, out string passwordHash, out string passwordSalt) +public class HashPassword +{ + public static string CreatePasswordHash(string password) { - using var hmac = new HMACSHA512(); - passwordSalt = Convert.ToBase64String(hmac.Key); - passwordHash = Convert.ToBase64String(hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password))); + var salt = new byte[16]; + RandomNumberGenerator.Fill(salt); + + var hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2( + password: password, + salt: salt, + prf: KeyDerivationPrf.HMACSHA256, + iterationCount: 10000, + numBytesRequested: 256 / 8)); + + return Convert.ToBase64String(salt) + ":" + hashed; } - public static bool VerifyPasswordHash(string password, string storedHash, string storedSalt) + public static bool VerifyPasswordHash(string hashedPassword, string password) { - using var hmac = new HMACSHA512(Convert.FromBase64String(storedSalt)); - var computedHash = Convert.ToBase64String(hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password))); - return computedHash == storedHash; + var parts = hashedPassword.Split(':'); + var salt = Convert.FromBase64String(parts[0]); + var storedHash = parts[1]; + + var hash = Convert.ToBase64String(KeyDerivation.Pbkdf2( + password: password, + salt: salt, + prf: KeyDerivationPrf.HMACSHA256, + iterationCount: 10000, + numBytesRequested: 256 / 8)); + + return hash == storedHash; } } \ No newline at end of file diff --git a/Helpers/JwtHelper.cs b/Helpers/JwtHelper.cs new file mode 100644 index 0000000..7d8ebb8 --- /dev/null +++ b/Helpers/JwtHelper.cs @@ -0,0 +1,75 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using apekade.Models; + +namespace apekade.Helpers; + +public class JwtHelper +{ + private readonly IConfiguration _configuration; + + public JwtHelper(IConfiguration configuration) + { + _configuration = configuration; + } + + public string GenerateJwt(User user) + { + var tokenHandler = new JwtSecurityTokenHandler(); + + var appSettingToken = _configuration.GetSection("AppSettings:Token").Value; + if (appSettingToken is null) + throw new Exception("AppSettings Token is null!"); + + var key = Encoding.UTF8.GetBytes(appSettingToken); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()) + }), + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } + + public ClaimsPrincipal? ValidateToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var appSettingToken = _configuration.GetSection("AppSettings:Token").Value; + if (appSettingToken is null) + throw new Exception("AppSettings Token is null!"); + var key = Encoding.UTF8.GetBytes(appSettingToken); + + try + { + var tokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false, + ClockSkew = TimeSpan.Zero // Remove delay of token when expire + }; + + // Validate the token and return the claims principal + var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken validatedToken); + return principal; // Return the ClaimsPrincipal + } + catch (Exception) + { + // Token is invalid + return null; + } + } + + +} \ No newline at end of file diff --git a/Middleware/ExceptionMiddleware.cs b/Middleware/ExceptionMiddleware.cs new file mode 100644 index 0000000..e5f907b --- /dev/null +++ b/Middleware/ExceptionMiddleware.cs @@ -0,0 +1,32 @@ +using System; +using System.Net; +using apekade.Models.Response; + +namespace apekade.Middleware; + +public class ExceptionMiddleware +{ + private readonly RequestDelegate _next; + public ExceptionMiddleware(RequestDelegate next){ + _next = next; + } + public async Task InvokeAsync(HttpContext context){ + try + { + await _next(context); + } + catch (Exception e) + { + context.Response.ContentType = "application/json"; + context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; + var error = new ErrorRes{ + Code = context.Response.StatusCode, + Status = false, + Data= new {}, + Message = e.Message + }; + await context.Response.WriteAsync(error.ToString()); + } + } + +} diff --git a/Middleware/ValidationMiddleware.cs b/Middleware/ValidationMiddleware.cs new file mode 100644 index 0000000..ca53973 --- /dev/null +++ b/Middleware/ValidationMiddleware.cs @@ -0,0 +1,46 @@ +using System; +using apekade.Models.Response; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Newtonsoft.Json; +namespace apekade.Middleware; + +public class ValidationMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ValidationMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + // Proceed with the next middleware in the pipeline + await _next(context); + + // Check if there are validation errors + if (!context.Response.HasStarted && context.Response.StatusCode == 400 && context.Items.ContainsKey("ModelState")) + { + var modelState = context.Items["ModelState"] as ModelStateDictionary; + if (modelState != null && !modelState.IsValid) + { + var firstError = modelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage) + .FirstOrDefault(); + + var errorResponse = new ErrorRes{ + Code = 400, + Status = false, + Message = "Validation errors", + Data = new { Error = firstError } + }; + + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(errorResponse.ToString()); + } + } + } +} diff --git a/Models/Dto/ApiRes.cs b/Models/Dto/ApiRes.cs deleted file mode 100644 index 95029c3..0000000 --- a/Models/Dto/ApiRes.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace apekade.Models.Dto; - -public class ApiRes{ - public Boolean? Status {get;set;} - public int Code {get;set;} - public T? Data {get;set;} - public string? Message { get; set; } -} \ No newline at end of file diff --git a/Models/Dto/AuthDto/LoginDto.cs b/Models/Dto/AuthDto/LoginDto.cs new file mode 100644 index 0000000..b19ecde --- /dev/null +++ b/Models/Dto/AuthDto/LoginDto.cs @@ -0,0 +1,9 @@ +#nullable disable +namespace apekade.Models.Dto.AuthDto; + +public class LoginDto +{ + public string Email { get; set; } + public string Password { get; set; } + public string Role { get; set; } +} diff --git a/Models/Dto/AuthDto/RegisterDto.cs b/Models/Dto/AuthDto/RegisterDto.cs new file mode 100644 index 0000000..38c1310 --- /dev/null +++ b/Models/Dto/AuthDto/RegisterDto.cs @@ -0,0 +1,11 @@ +#nullable disable +namespace apekade.Models.Dto.AuthDto; + +public class RegisterDto +{ + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public string Password { get; set; } + public string Role { get; set; } +} diff --git a/Models/Dto/AuthDto/RegisterResDto.cs b/Models/Dto/AuthDto/RegisterResDto.cs new file mode 100644 index 0000000..c63a080 --- /dev/null +++ b/Models/Dto/AuthDto/RegisterResDto.cs @@ -0,0 +1,10 @@ +#nullable disable +namespace apekade.Models.Dto.AuthDto; + +public class RegisterResDto +{ + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public string Role { get; set; } +} diff --git a/Models/Dto/UserDto/UserReqDto.cs b/Models/Dto/UserDto/UserReqDto.cs deleted file mode 100644 index 18da596..0000000 --- a/Models/Dto/UserDto/UserReqDto.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using apekade.Enums; - -namespace apekade.Models.Dto.UserDto; - -public class UserReqtDto -{ - [Required] - public required string FirstName { get; set; } - public string? LastName { get; set; } - - [Required] - [EmailAddress] - public required string Email { get; set; } - - [Required] - public required string Password { get; set; } - - [Required] - public Role Role { get; set; } -} \ No newline at end of file diff --git a/Models/Dto/UserDto/UserResDto.cs b/Models/Dto/UserDto/UserResDto.cs deleted file mode 100644 index c455751..0000000 --- a/Models/Dto/UserDto/UserResDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using apekade.Enums; - -namespace apekade.Models.Dto.UserDto; - -public class UserResDto -{ - [Required] - public required string FirstName { get; set; } - public string? LastName { get; set; } - - [EmailAddress] - [Required] - public required string Email { get; set; } - - [Required] - public Role Role { get; set; } -} \ No newline at end of file diff --git a/Models/Dto/UserDto/UserTokenResDto.cs b/Models/Dto/UserDto/UserTokenResDto.cs deleted file mode 100644 index 4a221d9..0000000 --- a/Models/Dto/UserDto/UserTokenResDto.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace apekade.Models.Dto.UserDto; - -public class UserTokenResDto -{ - public required UserResDto User { get; set; } - public required string Token { get; set; } -} \ No newline at end of file diff --git a/Enums/Role.cs b/Models/Enums/Role.cs similarity index 100% rename from Enums/Role.cs rename to Models/Enums/Role.cs diff --git a/Models/Filter/ExceptionFilter.cs b/Models/Filter/ExceptionFilter.cs new file mode 100644 index 0000000..da480bd --- /dev/null +++ b/Models/Filter/ExceptionFilter.cs @@ -0,0 +1,28 @@ +using System; +using apekade.Models.Response; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace apekade.Models.Filter; + +public class ExceptionFilter : IExceptionFilter +{ + public void OnException(ExceptionContext context) + { + var httpContext = context.HttpContext; + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + + var error = new ErrorRes + { + Code = httpContext.Response.StatusCode, + Status = false, + Data = new { }, + Message = context.Exception.Message + }; + context.Result = new JsonResult(error) + { + StatusCode = 500, + ContentType = "application/json" + }; + } +} diff --git a/Models/Response/ApiRes.cs b/Models/Response/ApiRes.cs new file mode 100644 index 0000000..6614d7c --- /dev/null +++ b/Models/Response/ApiRes.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc; +using JsonConvert = Newtonsoft.Json.JsonConvert; + +namespace apekade.Models.Dto +{ + public class ApiRes + { + public bool Status { get; set; } + public int Code { get; set; } + public string Message { get; set; } + public object? Data { get; set; } + public string TimeStamp { get; set; } + + public ApiRes(int code, bool status, string message, object? data) + { + Status = status; + Code = code; + Message = message; + Data = data ?? new { error = "Unknown error." }; + TimeStamp = DateTime.UtcNow.ToString("o"); + } + } + public static class ResExtension + { + public static IActionResult ApiRes(this ControllerBase controller,int code,bool status, string message, object? data) + { + var response = new ApiRes(code,status, message, data ?? new { error = "Unknown error." }); + return new ContentResult + { + Content = JsonConvert.SerializeObject(response), + ContentType = "application/json", + StatusCode = code + }; + } + } + +} diff --git a/Models/Response/ErrorRes.cs b/Models/Response/ErrorRes.cs new file mode 100644 index 0000000..bf928cd --- /dev/null +++ b/Models/Response/ErrorRes.cs @@ -0,0 +1,22 @@ +using System; +using System.Text.Json; + +namespace apekade.Models.Response; + +public class ErrorRes +{ + public bool Status { get; set; } + public int Code { get; set; } + public string? Message { get; set; } + public object? Data { get; set; } + public string TimeStamp { get; set; } + + public ErrorRes() + { + TimeStamp = DateTime.UtcNow.ToString("o"); // "o" for ISO 8601 format + } + public override string ToString() + { + return JsonSerializer.Serialize(this); + } +} diff --git a/Models/User.cs b/Models/User.cs index 56b098a..d489fe3 100644 --- a/Models/User.cs +++ b/Models/User.cs @@ -12,7 +12,6 @@ public class User{ public string? LastName { get; set; } public required string Email { get; set; } public required string PasswordHash { get; set; } - public required string PasswordSalt { get; set; } [BsonRepresentation((BsonType.String))] public required Role Role { get; set; } } diff --git a/Models/Validation/LoginValidator.cs b/Models/Validation/LoginValidator.cs new file mode 100644 index 0000000..e719e40 --- /dev/null +++ b/Models/Validation/LoginValidator.cs @@ -0,0 +1,32 @@ +using System; +using apekade.Enums; +using apekade.Models.Dto.AuthDto; +using FluentValidation; + +namespace apekade.Models.Validation; + +public class LoginValidator : AbstractValidator +{ + public LoginValidator() + { + RuleFor(x => x.Email) + .NotEmpty() + .EmailAddress() + .WithMessage("A valid email is required."); + + RuleFor(x => x.Password) + .NotEmpty() + .MinimumLength(6) + .WithMessage("Password must be at least 6 characters."); + + RuleFor(x => x.Role) + .NotEmpty() + .WithMessage("role should not be empty") + .Must(IsValidRole) + .WithMessage("role not valid ."); + } + private bool IsValidRole(string role) + { + return Enum.TryParse(typeof(Role), role, true, out _); + } +} diff --git a/Models/Validation/RegisterValidator.cs b/Models/Validation/RegisterValidator.cs new file mode 100644 index 0000000..696e88b --- /dev/null +++ b/Models/Validation/RegisterValidator.cs @@ -0,0 +1,38 @@ +using System; +using apekade.Models.Dto.AuthDto; +using FluentValidation; +using System.Linq; +using apekade.Enums; + +namespace apekade.Models.Validation; + +public class RegisterValidator : AbstractValidator +{ + public RegisterValidator() + { + RuleFor(x => x.FirstName) + .NotEmpty() + .WithMessage("First name is required."); + + RuleFor(x => x.Email) + .NotEmpty() + .EmailAddress() + .WithMessage("A valid email is required."); + + RuleFor(x => x.Password) + .NotEmpty() + .MinimumLength(6) + .WithMessage("Password must be at least 6 characters."); + + RuleFor(x => x.Role) + .NotEmpty() + .WithMessage("role should not be empty") + .Must(IsValidRole) + .WithMessage("role not valid ."); // Adjust based on your enum values + } + + private bool IsValidRole(string role) + { + return Enum.TryParse(typeof(Role), role, true, out _); + } +} diff --git a/Program.cs b/Program.cs index 46690f3..7e92378 100644 --- a/Program.cs +++ b/Program.cs @@ -1,5 +1,5 @@ using apekade.Configuration; -using apekade.Models.Dto; +using apekade.Middleware; var builder = WebApplication.CreateBuilder(args); @@ -20,16 +20,20 @@ // defined cors option // app.UseCors("AllowAll"); -app.UseHttpsRedirection(); +// comented to avoid redirect to https in not secure servers. +// app.UseHttpsRedirection(); + +// use Custom middlewares +app.UseMiddleware(); +// app.UseMiddleware(); // mapping controllers app.MapControllers(); // root server online status -app.MapGet("/", () => new ApiRes -{ - Status = true, +app.MapGet("/", () => new { Code = 200, + Status = true, Data = new { Message = "server_online" } }); diff --git a/Repositories/UserRepository.cs b/Repositories/UserRepository.cs index 2e37ce1..08c47a2 100644 --- a/Repositories/UserRepository.cs +++ b/Repositories/UserRepository.cs @@ -1,19 +1,29 @@ using MongoDB.Driver; using apekade.Models; +using apekade.Enums; namespace apekade.Repositories; public class UserRepository { - private readonly IMongoCollection _usersCollection; - + private readonly IMongoCollection _users; public UserRepository(IMongoDatabase database) { - _usersCollection = database.GetCollection("Users"); + _users = database.GetCollection("Users"); + } + // method to save a new user + public async Task Save(User user){ + await _users.InsertOneAsync(user); } - public async Task Save(User user) - { - await _usersCollection.InsertOneAsync(user); + //method to check if user existing + public async Task GetUserByEmail(string email){ + return await _users.Find(user => user.Email == email).FirstOrDefaultAsync(); } + + //method to check if a user exists by email and role + public async Task GetUserByEmailAndRole(string email , string role){ + return await _users.Find(user=> user.Email ==email && user.Role.ToString() == role).FirstOrDefaultAsync(); + } + } \ No newline at end of file diff --git a/Services/IAuthService.cs b/Services/IAuthService.cs new file mode 100644 index 0000000..7e2fdfb --- /dev/null +++ b/Services/IAuthService.cs @@ -0,0 +1,9 @@ +using apekade.Models.Dto; +using apekade.Models.Dto.AuthDto; + +namespace apekade.Services; + +public interface IAuthService{ + Task Register(RegisterDto registerDto); + Task Login(LoginDto loginDto); +} \ No newline at end of file diff --git a/Services/IUserService.cs b/Services/IUserService.cs index 1aaf014..fcfda64 100644 --- a/Services/IUserService.cs +++ b/Services/IUserService.cs @@ -1,9 +1,9 @@ using apekade.Models.Dto; -using apekade.Models.Dto.UserDto; namespace apekade.Services; public interface IUserService { - Task> CreateNewUser(UserReqtDto userReqtDto); + // Task CreateNewUser(UserReqtDto userReqtDto); + Task CreateNewUser(string email); } \ No newline at end of file diff --git a/Services/Impl/AuthService.cs b/Services/Impl/AuthService.cs new file mode 100644 index 0000000..3c832fa --- /dev/null +++ b/Services/Impl/AuthService.cs @@ -0,0 +1,76 @@ +using apekade.Models.Dto; +using apekade.Repositories; +using apekade.Helpers; +using apekade.Services.Impl; +using AutoMapper; +using apekade.Models.Dto.AuthDto; +using apekade.Models; + +namespace apekade.Services.Impl; + +public class AuthService : IAuthService +{ + private readonly IMapper _mapper; + private readonly UserRepository _userRepository; + private readonly JwtHelper _jwtHelper; + + public AuthService(IMapper mapper, UserRepository userRepository, JwtHelper jwtHelper) + { + _mapper = mapper; + _userRepository = userRepository; + _jwtHelper = jwtHelper; + } + public async Task Login(LoginDto loginDto) + { + var user = await _userRepository.GetUserByEmailAndRole(loginDto.Email,loginDto.Role); + if (user == null) + { + return new ApiRes(404, false, "user not found", new { }); + } + if (!HashPassword.VerifyPasswordHash(user.PasswordHash, loginDto.Password)) + { + return new ApiRes(403, false, "Password incorrect", new { }); + } + var token = _jwtHelper.GenerateJwt(user); + return new ApiRes(200, true, "login succcess", new { user, token }); + } + + public async Task Register(RegisterDto registerDto) + { + try + { + //check if the user exists in the DB + var existingUser = await _userRepository.GetUserByEmail(registerDto.Email); + if (existingUser != null) + { + return new ApiRes( + 409, + false, + "User already exists.", + new { } + ); + } + var newUser = _mapper.Map(registerDto); + newUser.PasswordHash = HashPassword.CreatePasswordHash(registerDto.Password); + await _userRepository.Save(newUser); + var token = _jwtHelper.GenerateJwt(newUser); + var userResponse = _mapper.Map(newUser); + + return new ApiRes( + 201, + true, + "User created successfully!", + new { userResponse, token } + ); + } + catch (Exception ex) + { + return new ApiRes( + 500, + false, + ex.Message, + new { } + ); + } + } +} \ No newline at end of file diff --git a/Services/Impl/UserService.cs b/Services/Impl/UserService.cs index 1302466..f45b889 100644 --- a/Services/Impl/UserService.cs +++ b/Services/Impl/UserService.cs @@ -4,54 +4,24 @@ using apekade.Repositories; using apekade.Helpers; using apekade.Services.Impl; -using apekade.Models.Dto.UserDto; namespace apekade.Services.Impl; public class UserService : IUserService { private readonly IMapper _mapper; - private readonly UserRepository _userRepository; + private readonly JwtHelper _generateJwtToken; - private readonly GenerateJwtToken _generateJwtToken; - - public UserService(IMapper mapper, UserRepository userRepository, GenerateJwtToken generateJwtToken) + public UserService(IMapper mapper, UserRepository userRepository, JwtHelper generateJwtToken) { _mapper = mapper; _userRepository = userRepository; _generateJwtToken = generateJwtToken; } - public async Task> CreateNewUser(UserReqtDto userRequestDto) + public Task CreateNewUser(string email) { - var response = new ApiRes(); - - try - { - var newUser = _mapper.Map(userRequestDto); - HashPassword.CreatePasswordHash(userRequestDto.Password, out var passwordHash, out var passwordSalt); - newUser.PasswordHash = passwordHash; - newUser.PasswordSalt = passwordSalt; - - await _userRepository.Save(newUser); - - var token = _generateJwtToken.GenerateJwt(newUser); - - var userResponse = _mapper.Map(newUser); - - response.Status = true; - response.Code = 201; - response.Data = new UserTokenResDto { User = userResponse, Token = token }; - response.Message = "User created successfully!"; - } - catch (Exception ex) - { - response.Status = false; - response.Code = 500; - response.Message = ex.Message; - } - - return response; + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/apekade.csproj b/apekade.csproj index bfe2056..6796ddf 100644 --- a/apekade.csproj +++ b/apekade.csproj @@ -15,9 +15,14 @@ + + + + + diff --git a/apekade.http b/apekade.http deleted file mode 100644 index b043c04..0000000 --- a/apekade.http +++ /dev/null @@ -1,6 +0,0 @@ -@apekade_HostAddress = http://localhost:5259 - -GET {{apekade_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/appsettings.json b/appsettings.json index 204c83d..0fa54e2 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,11 +6,11 @@ } }, "DbSettings": { - "ConnectionString": "mongodb://localhost:27017", - "DatabaseName": "YourDatabaseName" + "ConnectionString": "mongodb+srv://dabedip595:TczJPPrdHBzxcKKz@apekade.n8jqr9w.mongodb.net/ape_kade?retryWrites=true&w=majority&appName=apekade", + "DatabaseName": "ape_kade" }, "AppSettings": { - "Token": "jwt_secret" + "Token": "WW91clN0cm9uZ2VyU2VjcmV0S2V5VGhhdElzQXRMZWFzdDE2Q2hhcmFjdGVycw==" }, "AllowedHosts": "*" } diff --git a/configuration/JwtConfiguration.cs b/configuration/JwtConfiguration.cs index 05e0591..bcd577f 100644 --- a/configuration/JwtConfiguration.cs +++ b/configuration/JwtConfiguration.cs @@ -1,4 +1,5 @@ using System.Text; +using apekade.Models.Response; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Swashbuckle.AspNetCore.Filters; @@ -27,27 +28,37 @@ public static void ConfigureJwtServices(IServiceCollection services, IConfigurat { OnChallenge = context => { - // If the token is missing, return a custom message - if (string.IsNullOrEmpty(context.Request.Headers.Authorization)) - { - context.HandleResponse(); - context.Response.StatusCode = 401; - context.Response.ContentType = "application/json"; - return context.Response.WriteAsync("{\"error\": \"Authorization header is missing. Please provide a valid Bearer token.\"}"); - } + context.HandleResponse(); // Suppress the default response - // Handle invalid token case (for example, token is present but invalid) - context.HandleResponse(); + // Create the custom response context.Response.StatusCode = 401; context.Response.ContentType = "application/json"; - return context.Response.WriteAsync("{\"error\": \"You are not authorized to access this resource. Invalid or missing token.\"}"); + var response = new ErrorRes + { + Status = false, + Code = 401, + Message = string.IsNullOrEmpty(context.Request.Headers.Authorization) + ? "Authorization header is missing." + : "Invalid or missing token.", + Data = new { } + }; + return context.Response.WriteAsync(response.ToString()); + // return context.Response.WriteAsync(JsonSerializer.Serialize(response)); }, OnForbidden = context => - { - context.Response.StatusCode = 403; - context.Response.ContentType = "application/json"; - return context.Response.WriteAsync("{\"error\": \"You do not have access to this resource.\"}"); - } + { + context.Response.StatusCode = 403; + context.Response.ContentType = "application/json"; + var response = new ErrorRes + { + Status = false, + Code = 403, + Message = "You do not have access to this resource.", + Data = new { } + }; + return context.Response.WriteAsync(response.ToString()); + // return context.Response.WriteAsync(JsonSerializer.Serialize(response)); + } }; }); // Configure Swagger to handle JWT Bearer token diff --git a/configuration/MapperConfig.cs b/configuration/MapperConfig.cs index 43e2312..d510df1 100644 --- a/configuration/MapperConfig.cs +++ b/configuration/MapperConfig.cs @@ -1,6 +1,6 @@ using AutoMapper; using apekade.Models; -using apekade.Models.Dto.UserDto; +using apekade.Models.Dto.AuthDto; namespace apekade.Configuration; @@ -8,7 +8,10 @@ public class MapperConfig : Profile { public MapperConfig() { - CreateMap(); - CreateMap(); + // CreateMap(); + // CreateMap(); + + CreateMap(); + CreateMap(); } } \ No newline at end of file diff --git a/configuration/ServiceConfiguration.cs b/configuration/ServiceConfiguration.cs index 585ce0a..fc09ddf 100644 --- a/configuration/ServiceConfiguration.cs +++ b/configuration/ServiceConfiguration.cs @@ -1,6 +1,6 @@ using System.Reflection; -using apekade.Models.Dto.UserDto; using apekade.Helpers; +using apekade.Models.Filter; using apekade.Repositories; using apekade.Services; using apekade.Services.Impl; @@ -15,7 +15,17 @@ public static class ServiceConfiguration public static void Configure(IServiceCollection services, IConfiguration configuration) { // Add controllers - services.AddControllers(); + services.AddControllers( + // add custom exception filter + // cfg=>{ + // cfg.Filters.Add(typeof(ExceptionFilter)); + // } + ).ConfigureApiBehaviorOptions(options =>{ + //disables the default behaviour of the [ApiController] + options.SuppressModelStateInvalidFilter = true; + }).AddFluentValidation(v=>{ + v.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly()); + }); // Add services to the container. services.AddEndpointsApiExplorer(); @@ -25,9 +35,10 @@ public static void Configure(IServiceCollection services, IConfiguration configu // Register services services.AddScoped(); + services.AddScoped(); // register helpers with DI - services.AddScoped(); + services.AddScoped(); // register repositories services.AddScoped();