diff --git a/Controllers/StatusController.cs b/Controllers/StatusController.cs deleted file mode 100644 index 54ab15f..0000000 --- a/Controllers/StatusController.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace apekade.Controllers; - -[Route("api/[controller]")] -[ApiController] -// [Authorize] -public class StatusController : ControllerBase -{ - - [HttpGet] - public IActionResult GetServerStatus() - { - var response = new ApiRes{ - Status = "Success", - Code= 200, - Data = new { Message = "Server Online" } - }; - return Ok(response); - - } -} \ No newline at end of file diff --git a/Controllers/TestController.cs b/Controllers/TestController.cs new file mode 100644 index 0000000..61fc624 --- /dev/null +++ b/Controllers/TestController.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Mvc; +using apekade.Dto; +using Microsoft.AspNetCore.Authorization; + +namespace apekade.Controllers; + +[Route("api/[controller]")] +[ApiController] +// [Authorize] +public class TestController : ControllerBase +{ + + [HttpGet] + public IActionResult GetServerStatus() + { + var response = new ApiRes{ + Status = true, + Code= 200, + Data = new { Message = "Server Online" } + }; + return Ok(response); + + } + // Only SuperAdmin can access this endpoint + [Authorize(Roles = "SUPER_ADMIN")] + [HttpGet("superadmin")] + public IActionResult SuperAdminOnly() + { + return Ok("Only SuperAdmin can access this."); + } + + // Only Seller can access this endpoint + [Authorize(Roles = "SELLER")] + [HttpGet("seller")] + public IActionResult SellerOnly() + { + return Ok("Only Sellers can access this."); + } + + // Only Buyer can access this endpoint + [Authorize(Roles = "BUYER")] + [HttpGet("buyer")] + public IActionResult BuyerOnly() + { + return Ok("Only Buyers can access this."); + } + + // Both Seller and Buyer can access this endpoint + [Authorize(Roles = "SELLER,BUYER")] + [HttpGet("seller-buyer")] + public IActionResult SellerAndBuyerAccess() + { + return Ok("Both Sellers and Buyers can access this."); + } + + // Any authenticated user can access this endpoint + [Authorize] + [HttpGet("common")] + public IActionResult CommonAccess() + { + return Ok("Any authenticated user can access this."); + } + + // Any one can access this endpoint + [HttpGet("open")] + public IActionResult OpenAccess() + { + return Ok("Any one can access this."); + } +} \ No newline at end of file diff --git a/Controllers/UserController.cs b/Controllers/UserController.cs new file mode 100644 index 0000000..09f5530 --- /dev/null +++ b/Controllers/UserController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using apekade.Enums; +using apekade.Services; +using apekade.Dto.UserDto; + +namespace apekade.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class UserController : ControllerBase{ + private readonly IUserService _userService; + + public UserController(IUserService userService){ + _userService = userService; + } + + [HttpPost] + public async Task CreateUser([FromBody] UserReqtDto userReqtDto){ + + var response = await _userService.CreateNewUser(userReqtDto); + return Ok(response); + } +} \ No newline at end of file diff --git a/Enums/Role.cs b/Enums/Role.cs new file mode 100644 index 0000000..c7c2557 --- /dev/null +++ b/Enums/Role.cs @@ -0,0 +1,8 @@ +namespace apekade.Enums; + +public enum Role +{ + ADMIN, + SELLER, + BUYER +} \ No newline at end of file diff --git a/Helpers/GenerateJwtToken.cs b/Helpers/GenerateJwtToken.cs new file mode 100644 index 0000000..78950eb --- /dev/null +++ b/Helpers/GenerateJwtToken.cs @@ -0,0 +1,40 @@ +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.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 new file mode 100644 index 0000000..8b10e9b --- /dev/null +++ b/Helpers/HashPassword.cs @@ -0,0 +1,19 @@ +using System.Security.Cryptography; + +namespace apekade.Helpers; + +public class HashPassword{ + public static void CreatePasswordHash(string password, out string passwordHash, out string passwordSalt) + { + using var hmac = new HMACSHA512(); + passwordSalt = Convert.ToBase64String(hmac.Key); + passwordHash = Convert.ToBase64String(hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password))); + } + + public static bool VerifyPasswordHash(string password, string storedHash, string storedSalt) + { + using var hmac = new HMACSHA512(Convert.FromBase64String(storedSalt)); + var computedHash = Convert.ToBase64String(hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password))); + return computedHash == storedHash; + } +} \ No newline at end of file diff --git a/Models/User.cs b/Models/User.cs new file mode 100644 index 0000000..56b098a --- /dev/null +++ b/Models/User.cs @@ -0,0 +1,18 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using apekade.Enums; + +namespace apekade.Models; + +public class User{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public required string Id { get; set; } + public required string FirstName { get; set; } + 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/Program.cs b/Program.cs index f11e811..04c4ecb 100644 --- a/Program.cs +++ b/Program.cs @@ -1,10 +1,10 @@ -var builder = WebApplication.CreateBuilder(args); +using apekade.Configuration; +using apekade.Dto; -// Add services to the container. -builder.Services.AddEndpointsApiExplorer(); +var builder = WebApplication.CreateBuilder(args); //the service configs are handled -ServiceConfiguration.Configure(builder.Services,builder.Configuration); +ServiceConfiguration.Configure(builder.Services, builder.Configuration); var app = builder.Build(); @@ -26,8 +26,9 @@ app.MapControllers(); // root server online status -app.MapGet("/",()=> new ApiRes{ - Status = "Success", +app.MapGet("/", () => new ApiRes +{ + Status = true, Code = 200, Data = new { Message = "Server_Online" } }); diff --git a/Repositories/UserRepository.cs b/Repositories/UserRepository.cs new file mode 100644 index 0000000..796df39 --- /dev/null +++ b/Repositories/UserRepository.cs @@ -0,0 +1,19 @@ +using MongoDB.Driver; +using apekade.Models; + +namespace apekade.Repositories; + +public class UserRepository +{ + private readonly IMongoCollection _usersCollection; + + public UserRepository(IMongoDatabase database) + { + _usersCollection = database.GetCollection("Users"); + } + + public async Task save(User user) + { + await _usersCollection.InsertOneAsync(user); + } +} \ No newline at end of file diff --git a/Services/IUserService.cs b/Services/IUserService.cs new file mode 100644 index 0000000..c425381 --- /dev/null +++ b/Services/IUserService.cs @@ -0,0 +1,9 @@ +using apekade.Dto; +using apekade.Dto.UserDto; + +namespace apekade.Services; + +public interface IUserService +{ + Task> CreateNewUser(UserReqtDto userReqtDto); +} \ No newline at end of file diff --git a/Services/Impl/UserService.cs b/Services/Impl/UserService.cs new file mode 100644 index 0000000..71a1c9f --- /dev/null +++ b/Services/Impl/UserService.cs @@ -0,0 +1,57 @@ +using AutoMapper; +using apekade.Models; +using apekade.Dto; +using apekade.Repositories; +using apekade.Helpers; +using apekade.Services.Impl; +using apekade.Dto.UserDto; + +namespace apekade.Services.Impl; + +public class UserService : IUserService +{ + private readonly IMapper _mapper; + + private readonly UserRepository _userRepository; + + private readonly GenerateJwtToken _generateJwtToken; + + public UserService(IMapper mapper, UserRepository userRepository, GenerateJwtToken generateJwtToken) + { + _mapper = mapper; + _userRepository = userRepository; + _generateJwtToken = generateJwtToken; + } + + public async Task> CreateNewUser(UserReqtDto userRequestDto) + { + 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; + } +} \ No newline at end of file diff --git a/apekade.csproj b/apekade.csproj index 51a31c3..bfe2056 100644 --- a/apekade.csproj +++ b/apekade.csproj @@ -9,12 +9,14 @@ + + diff --git a/configuration/CorsConfiguration.cs b/configuration/CorsConfiguration.cs index 4bfcbe1..4877c2b 100644 --- a/configuration/CorsConfiguration.cs +++ b/configuration/CorsConfiguration.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +namespace apekade.Configuration; public static class CorsConfiguration { diff --git a/configuration/DbContextConfiguration.cs b/configuration/DbContextConfiguration.cs index 852b389..80d8938 100644 --- a/configuration/DbContextConfiguration.cs +++ b/configuration/DbContextConfiguration.cs @@ -1,21 +1,33 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using MongoDB.Driver; +using apekade.Data; + +namespace apekade.Configuration; + public static class DbContextConfiguration { public static void ConfigureDbContextServices(IServiceCollection services, IConfiguration configuration) { - // Register the DbSettings configuration - services.Configure(configuration.GetSection(nameof(DbSettings))); + // Register the DbSettings configuration + services.Configure(configuration.GetSection(nameof(DbSettings))); - // Register the MongoDB DbContext - // services.AddSingleton(); - - services.AddSingleton(provider =>{ - var dbSettings = provider.GetService>() ?? throw new InvalidOperationException("MongoDbSettings is not configured properly"); + // Register the MongoDB DbContext + // service is created once for all. + services.AddSingleton(provider => + { + var dbSettings = provider.GetRequiredService>() ?? throw new InvalidOperationException("MongoDbSettings is not configured properly"); // Initialize DbContext with the DbSettings - return new DbContext(dbSettings); + return new DbContext(dbSettings); + }); + + // service is created once per HTTP request in web applications. + services.AddScoped(sp => + { + var settings = sp.GetRequiredService>().Value; + var client = sp.GetRequiredService(); + return client.GetDatabase(settings.DatabaseName); }); } } diff --git a/configuration/JwtConfiguration.cs b/configuration/JwtConfiguration.cs index 7b50504..05e0591 100644 --- a/configuration/JwtConfiguration.cs +++ b/configuration/JwtConfiguration.cs @@ -1,11 +1,66 @@ -public static class JwtConfiguration{ - public static void ConfigureJwtServices(IServiceCollection services, IConfiguration configuration) +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Swashbuckle.AspNetCore.Filters; + +namespace apekade.Configuration; + +public static class JwtConfiguration +{ + public static void ConfigureJwtServices(IServiceCollection services, IConfiguration configuration) { // Register JWT settings from "AppSettings" - var jwtSettings = configuration.GetSection("AppSettings").Get(); - services.AddSingleton(jwtSettings); + var key = Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value!); // If you have any additional JWT-related services or middleware, you can configure them here. - // Example: services.AddAuthentication(...).AddJwtBearer(...); + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false + }; + + options.Events = new JwtBearerEvents + { + 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.\"}"); + } + + // Handle invalid token case (for example, token is present but invalid) + context.HandleResponse(); + 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.\"}"); + }, + OnForbidden = context => + { + context.Response.StatusCode = 403; + context.Response.ContentType = "application/json"; + return context.Response.WriteAsync("{\"error\": \"You do not have access to this resource.\"}"); + } + }; + }); + // Configure Swagger to handle JWT Bearer token + services.AddSwaggerGen(c => + { + c.AddSecurityDefinition("oauth2", new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Description = """Standard Authorization header using the Bearer scheme. Example: "Bearer {token}" """, + In = Microsoft.OpenApi.Models.ParameterLocation.Header, + Name = "Authorization", + Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey + }); + c.OperationFilter(); + }); } } \ No newline at end of file diff --git a/configuration/MapperConfig.cs b/configuration/MapperConfig.cs new file mode 100644 index 0000000..41f2444 --- /dev/null +++ b/configuration/MapperConfig.cs @@ -0,0 +1,14 @@ +using AutoMapper; +using apekade.Models; +using apekade.Dto.UserDto; + +namespace apekade.Configuration; + +public class MapperConfig : Profile +{ + public MapperConfig() + { + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/configuration/ServiceConfiguration.cs b/configuration/ServiceConfiguration.cs index e162486..e309cd4 100644 --- a/configuration/ServiceConfiguration.cs +++ b/configuration/ServiceConfiguration.cs @@ -1,3 +1,15 @@ +using System.Reflection; +using apekade.Dto.UserDto; +using apekade.Helpers; +using apekade.Repositories; +using apekade.Services; +using apekade.Services.Impl; +using FluentValidation.AspNetCore; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace apekade.Configuration; + public static class ServiceConfiguration { public static void Configure(IServiceCollection services, IConfiguration configuration) @@ -5,6 +17,28 @@ public static void Configure(IServiceCollection services, IConfiguration configu // Add controllers services.AddControllers(); + // Add services to the container. + services.AddEndpointsApiExplorer(); + + //Add Automapper configs + services.AddAutoMapper(typeof(Program).Assembly); + + // Register services + services.AddScoped(); + + // register helpers with DI + services.AddScoped(); + + // register repositories + services.AddScoped(); + + // Register IMongoClient + services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + return new MongoClient(settings.ConnectionString); + }); + // Configure Swagger SwaggerConfiguration.ConfigureSwaggerServices(services); @@ -13,11 +47,8 @@ public static void Configure(IServiceCollection services, IConfiguration configu // Configure Identity and Authorization - // Register services - // services.AddScoped(); - //configure JWT - JwtConfiguration.ConfigureJwtServices(services,configuration); + JwtConfiguration.ConfigureJwtServices(services, configuration); // Configure CORS CorsConfiguration.ConfigureCorsServices(services); diff --git a/configuration/SwaggerConfiguration.cs b/configuration/SwaggerConfiguration.cs index ddde005..892e82b 100644 --- a/configuration/SwaggerConfiguration.cs +++ b/configuration/SwaggerConfiguration.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; +namespace apekade.Configuration; public static class SwaggerConfiguration { diff --git a/data/DbContext.cs b/data/DbContext.cs index 3d70662..dbe73f3 100644 --- a/data/DbContext.cs +++ b/data/DbContext.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.Options; using MongoDB.Driver; +using apekade.Models; +namespace apekade.Data; public class DbContext{ private readonly IMongoDatabase _database; @@ -10,5 +12,5 @@ public DbContext(IOptions options){ _database = client.GetDatabase(settings.DatabaseName); } //include all the mongo collections - // public IMongoCollection Users => _database.GetCollection("Users"); + public IMongoCollection Users => _database.GetCollection("Users"); } \ No newline at end of file diff --git a/dto/ApiRes.cs b/dto/ApiRes.cs index fc5ef4b..ef59c1b 100644 --- a/dto/ApiRes.cs +++ b/dto/ApiRes.cs @@ -1,5 +1,8 @@ +namespace apekade.Dto; + public class ApiRes{ - public string? Status {get;set;} + 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/dto/UserDto/UserReqDto.cs b/dto/UserDto/UserReqDto.cs new file mode 100644 index 0000000..bbbeaef --- /dev/null +++ b/dto/UserDto/UserReqDto.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using apekade.Enums; + +namespace apekade.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/dto/UserDto/UserResDto.cs b/dto/UserDto/UserResDto.cs new file mode 100644 index 0000000..0de9dbc --- /dev/null +++ b/dto/UserDto/UserResDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using apekade.Enums; + +namespace apekade.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/dto/UserDto/UserTokenResDto.cs b/dto/UserDto/UserTokenResDto.cs new file mode 100644 index 0000000..08437ac --- /dev/null +++ b/dto/UserDto/UserTokenResDto.cs @@ -0,0 +1,7 @@ +namespace apekade.Dto.UserDto; + +public class UserTokenResDto +{ + public required UserResDto User { get; set; } + public required string Token { get; set; } +} \ No newline at end of file