From 62ba58e3919614a6c5dbb5ea2d5b8042f73b2bc0 Mon Sep 17 00:00:00 2001 From: Lucas Theze Date: Sat, 27 Mar 2021 20:09:55 +0000 Subject: [PATCH] services, startup Jwt, Helpers --- BackEndAaaapero/Data/Context.cs | 2 + BackEndAaaapero/Helpers/AppExceptions.cs | 19 +++ BackEndAaaapero/Helpers/AutoMapperProfile.cs | 18 ++ BackEndAaaapero/Models/User.cs | 11 ++ BackEndAaaapero/Services/UserServices.cs | 163 +++++++++++++++++++ BackEndAaaapero/Startup.cs | 63 ++++++- 6 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 BackEndAaaapero/Helpers/AppExceptions.cs create mode 100644 BackEndAaaapero/Helpers/AutoMapperProfile.cs create mode 100644 BackEndAaaapero/Models/User.cs create mode 100644 BackEndAaaapero/Services/UserServices.cs diff --git a/BackEndAaaapero/Data/Context.cs b/BackEndAaaapero/Data/Context.cs index 06278f0..8957942 100644 --- a/BackEndAaaapero/Data/Context.cs +++ b/BackEndAaaapero/Data/Context.cs @@ -1,3 +1,4 @@ +using BackEndAaaapero.Models; using Microsoft.EntityFrameworkCore; using Models; namespace BackEndAaaapero.Data @@ -7,5 +8,6 @@ namespace BackEndAaaapero.Data public Context(DbContextOptions options) : base(options) {} public DbSet customers { get; set; } + public DbSet User {get; set; } } } \ No newline at end of file diff --git a/BackEndAaaapero/Helpers/AppExceptions.cs b/BackEndAaaapero/Helpers/AppExceptions.cs new file mode 100644 index 0000000..7b26752 --- /dev/null +++ b/BackEndAaaapero/Helpers/AppExceptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; + +namespace BackEndAaaapero.Helpers +{ + // Custom exception class for throwing application specific exceptions (e.g. for validation) + // that can be caught and handled within the application + public class AppException : Exception + { + public AppException() : base() {} + + public AppException(string message) : base(message) { } + + public AppException(string message, params object[] args) + : base(String.Format(CultureInfo.CurrentCulture, message, args)) + { + } + } +} \ No newline at end of file diff --git a/BackEndAaaapero/Helpers/AutoMapperProfile.cs b/BackEndAaaapero/Helpers/AutoMapperProfile.cs new file mode 100644 index 0000000..90349c9 --- /dev/null +++ b/BackEndAaaapero/Helpers/AutoMapperProfile.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using DTO; +using Models; +using BackEndAaaapero.Models; + +namespace BackEndAaaapero.Helpers +{ + public class AutoMapperProfile : Profile + { + public AutoMapperProfile() + { + // + CreateMap(); + CreateMap(); + CreateMap(); + } + } +} \ No newline at end of file diff --git a/BackEndAaaapero/Models/User.cs b/BackEndAaaapero/Models/User.cs new file mode 100644 index 0000000..aa2a806 --- /dev/null +++ b/BackEndAaaapero/Models/User.cs @@ -0,0 +1,11 @@ +namespace BackEndAaaapero.Models +{ + public class User + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Username { get; set; } + public string PasswordHash { get; set; } + } +} \ No newline at end of file diff --git a/BackEndAaaapero/Services/UserServices.cs b/BackEndAaaapero/Services/UserServices.cs new file mode 100644 index 0000000..4eb1b8a --- /dev/null +++ b/BackEndAaaapero/Services/UserServices.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using BackEndAaaapero.Data; +using BackEndAaaapero.Helpers; +using BackEndAaaapero.Models; + +namespace Services +{ + public interface IUserService + { + User Authenticate(string username, string password); + IEnumerable GetAll(); + User GetById(int id); + User Create(User user, string password); + void Update(User user, string currentPassword, string password, string confirmPassword); + void Delete(int id); + } + + public class UserService : IUserService + { + private Context _context; + + public UserService(Context context) + { + _context = context; + } + + public User Authenticate(string username, string password) + { + if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) + { + return null; + } + + var user = _context.User.FirstOrDefault(x => x.Username == username) ?? null; + + // check if username exists + if (user == null) + { + return null; + } + + // Granting access if the hashed password in the database matches with the password(hashed in computeHash method) entered by user. + if(computeHash(password) != user.PasswordHash) + { + return null; + } + return user; + } + + public IEnumerable GetAll() + { + return _context.User; + } + + public User GetById(int id) + { + return _context.User.Find(id); + } + + public User Create(User user, string password) + { + // validation + if (string.IsNullOrWhiteSpace(password)) + { + throw new AppException("Password is required"); + } + + if (_context.User.Any(x => x.Username == user.Username)) + { + throw new AppException("Username \"" + user.Username + "\" is already taken"); + } + + //Saving hashed password into Database table + user.PasswordHash = computeHash(password); + + _context.User.Add(user); + _context.SaveChanges(); + + return user; + } + + public void Update(User userParam, string currentPassword = null, string password = null, string confirmPassword = null) + { + //Find the user by Id + var user = _context.User.Find(userParam.Id); + + if (user == null) + { + throw new AppException("User not found"); + } + // update user properties if provided + if (!string.IsNullOrWhiteSpace(userParam.Username) && userParam.Username != user.Username) + { + // throw error if the new username is already taken + if (_context.User.Any(x => x.Username == userParam.Username)) + { + throw new AppException("Username " + userParam.Username + " is already taken"); + } + else + { + user.Username = userParam.Username; + } + } + if (!string.IsNullOrWhiteSpace(userParam.FirstName)) + { + user.FirstName = userParam.FirstName; + } + if (!string.IsNullOrWhiteSpace(userParam.LastName)) + { + user.LastName = userParam.LastName; + } + if (!string.IsNullOrWhiteSpace(currentPassword)) + { + if(computeHash(currentPassword) != user.PasswordHash) + { + throw new AppException("Invalid Current password!"); + } + + if(currentPassword == password) + { + throw new AppException("Please choose another password!"); + } + + if(password != confirmPassword) + { + throw new AppException("Password doesn't match!"); + } + + //Updating hashed password into Database table + user.PasswordHash = computeHash(password); + } + + _context.User.Update(user); + _context.SaveChanges(); + } + + public void Delete(int id) + { + var user = _context.User.Find(id); + if (user != null) + { + _context.User.Remove(user); + _context.SaveChanges(); + } + } + + private static string computeHash(string Password) + { + MD5 md5 = new MD5CryptoServiceProvider(); + var input = md5.ComputeHash(Encoding.UTF8.GetBytes(Password)); + var hashstring = ""; + foreach(var hashbyte in input) + { + hashstring += hashbyte.ToString("x2"); + } + return hashstring; + } + } +} \ No newline at end of file diff --git a/BackEndAaaapero/Startup.cs b/BackEndAaaapero/Startup.cs index 69b32c0..055ba2b 100644 --- a/BackEndAaaapero/Startup.cs +++ b/BackEndAaaapero/Startup.cs @@ -1,17 +1,17 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using BackEndAaaapero.Data; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System.Text; +using AutoMapper; +using Services; namespace BackEndAaaapero { @@ -31,8 +31,52 @@ namespace BackEndAaaapero { opt.UseMySql(Configuration.GetConnectionString("DefaultConnection")); }); - + + //-------------------------------------------------------------------------------- + + services.AddCors(); services.AddControllers(); + services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + + // configure jwt authentication + var key = Encoding.ASCII.GetBytes(Configuration["Secret"]); + services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(x => + { + x.Events = new JwtBearerEvents + { + OnTokenValidated = context => + { + var userService = context.HttpContext.RequestServices.GetRequiredService(); + var userId = int.Parse(context.Principal.Identity.Name); + var user = userService.GetById(userId); + if (user == null) + { + // return unauthorized if user no longer exists + context.Fail("Unauthorized"); + } + return Task.CompletedTask; + } + }; + x.RequireHttpsMetadata = false; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false + }; + }); + + // configure DI for application services + services.AddScoped(); + //------------------------------------------------------------------------ + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -43,10 +87,17 @@ namespace BackEndAaaapero app.UseDeveloperExceptionPage(); } + app.UseCors(x => x + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + app.UseHttpsRedirection(); app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); app.UseEndpoints(endpoints =>