diff --git a/BusinessLogic/BusinessLogic.csproj b/BusinessLogic/BusinessLogic.csproj index d0efbf0..c105ec7 100644 --- a/BusinessLogic/BusinessLogic.csproj +++ b/BusinessLogic/BusinessLogic.csproj @@ -6,8 +6,17 @@ enable + + + + - + + + + + + diff --git a/BusinessLogic/BusinessLogic/RoleLogic.cs b/BusinessLogic/BusinessLogic/RoleLogic.cs new file mode 100644 index 0000000..9be0c6c --- /dev/null +++ b/BusinessLogic/BusinessLogic/RoleLogic.cs @@ -0,0 +1,96 @@ +using Contracts.BindingModels; +using Contracts.BusinessLogicContracts; +using Contracts.Converters; +using Contracts.Exceptions; +using Contracts.SearchModels; +using Contracts.StorageContracts; +using Contracts.ViewModels; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BusinessLogic.BusinessLogic +{ + public class RoleLogic : IRoleLogic + { + private readonly ILogger _logger; + private readonly IRoleStorage _roleStorage; + + public RoleLogic(ILogger logger, IRoleStorage roleStorage) + { + _logger = logger; + _roleStorage = roleStorage; + } + + public RoleViewModel Create(RoleBindingModel model) + { + ArgumentNullException.ThrowIfNull(model); + + var role = _roleStorage.Insert(model); + if (role is null) + { + throw new Exception("Insert operation failed."); + } + + return RoleConverter.ToView(role); + } + + public RoleViewModel Delete(RoleSearchModel model) + { + ArgumentNullException.ThrowIfNull(model); + + _logger.LogInformation("Delete role. Id: {0}", model.Id); + var role = _roleStorage.Delete(model); + if (role is null) + { + throw new Exception("Delete operation failed."); + } + + return RoleConverter.ToView(role); + } + + public RoleViewModel ReadElement(RoleSearchModel model) + { + ArgumentNullException.ThrowIfNull(model); + + _logger.LogInformation("ReadElement. Id: {0}", model.Id); + var role = _roleStorage.GetElement(model); + if (role is null) + { + throw new ElementNotFoundException(); + } + _logger.LogInformation("ReadElement find. Id: {0}", role.Id); + + return RoleConverter.ToView(role); + } + + public IEnumerable ReadElements(RoleSearchModel? model) + { + _logger.LogInformation("ReadList. Id: {Id}", model?.Id); + var list = _roleStorage.GetList(model); + if (list is null || list.Count() == 0) + { + _logger.LogWarning("ReadList return null list"); + return []; + } + _logger.LogInformation("ReadList. Count: {Count}", list.Count()); + + return list.Select(RoleConverter.ToView); + } + + public RoleViewModel Update(RoleBindingModel model) + { + ArgumentNullException.ThrowIfNull(model); + + var role = _roleStorage.Update(model); + if (role is null) + { + throw new Exception("Update operation failed."); + } + return RoleConverter.ToView(role); + } + } +} \ No newline at end of file diff --git a/BusinessLogic/BusinessLogic/UserLogic.cs b/BusinessLogic/BusinessLogic/UserLogic.cs new file mode 100644 index 0000000..b028660 --- /dev/null +++ b/BusinessLogic/BusinessLogic/UserLogic.cs @@ -0,0 +1,134 @@ +using BusinessLogic.Tools; +using Contracts.BindingModels; +using Contracts.BusinessLogicContracts; +using Contracts.Converters; +using Contracts.Exceptions; +using Contracts.SearchModels; +using Contracts.StorageContracts; +using Contracts.ViewModels; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BusinessLogic.BusinessLogic +{ + public class UserLogic : IUserLogic + { + private readonly ILogger _logger; + private readonly IUserStorage _userStorage; + + public UserLogic(ILogger logger, IUserStorage userStorage) + { + _logger = logger; + _userStorage = userStorage; + } + + public UserViewModel Create(UserBindingModel model) + { + ArgumentNullException.ThrowIfNull(model); + // Проверяем пароль + _validatePassword(model.Password); + // Хешируем пароль + model.PasswordHash = PasswordHasher.Hash(model.Password); + var user = _userStorage.Insert(model); + if (user is null) + { + throw new Exception("Insert operation failed."); + } + + return UserConverter.ToView(user); + } + + public UserViewModel Delete(UserSearchModel model) + { + ArgumentNullException.ThrowIfNull(model); + + _logger.LogInformation("Delete user. Id: {0}", model.Id); + var user = _userStorage.Delete(model); + if (user is null) + { + throw new ElementNotFoundException(); + } + + return UserConverter.ToView(user); + } + + public IEnumerable ReadElements(UserSearchModel? model) + { + _logger.LogInformation("ReadList. Id: {Id}", model?.Id); + var list = _userStorage.GetList(model); + if (list is null || list.Count() == 0) + { + _logger.LogWarning("ReadList return null list"); + return []; + } + _logger.LogInformation("ReadList. Count: {Count}", list.Count()); + + return list.Select(UserConverter.ToView); + } + + public UserViewModel ReadElement(UserSearchModel model) + { + ArgumentNullException.ThrowIfNull(model); + + _logger.LogInformation("ReadElement. Id: {0}", model.Id); + var user = _userStorage.GetElement(model); + if (user is null) + { + throw new ElementNotFoundException(); + } + _logger.LogInformation("ReadElement find. Id: {0}", user.Id); + + return UserConverter.ToView(user); + } + + public UserViewModel Update(UserBindingModel model) + { + ArgumentNullException.ThrowIfNull(model); + + if (model.Password is not null) + { + _validatePassword(model.Password); + model.PasswordHash = PasswordHasher.Hash(model.Password); + } + var user = _userStorage.Update(model); + if (user is null) + { + throw new Exception("Update operation failed."); + } + return UserConverter.ToView(user); + } + + public UserViewModel Login(string email, string password) + { + if (email is null) + { + throw new AccountException("Email is null"); + } + var user = _userStorage.GetElement(new() { Email = email }); + + if (user is null) + { + throw new ElementNotFoundException(); + } + // Проверяем пароль + _validatePassword(password); + if (!PasswordHasher.Verify(password, user.PasswordHash)) + { + throw new AccountException("The passwords don't match."); + } + return UserConverter.ToView(user); + } + + public void _validatePassword(string? password) + { + if (string.IsNullOrWhiteSpace(password)) + { + throw new AccountException("The password is null."); + } + } + } +} \ No newline at end of file diff --git a/BusinessLogic/Tools/PasswordHasher.cs b/BusinessLogic/Tools/PasswordHasher.cs new file mode 100644 index 0000000..f52c90f --- /dev/null +++ b/BusinessLogic/Tools/PasswordHasher.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace BusinessLogic.Tools +{ + internal class PasswordHasher + { + /// + /// Хеширует с использование SHA256 + /// + /// Пароль + /// Хеш пароля + public static string Hash(string password) + { + return BCrypt.Net.BCrypt.HashPassword(password); + } + + /// + /// Проверяет на соответствие пароля и его хеша + /// + /// Пароль + /// Хеш пароля + /// + public static bool Verify(string password, string passHash) + { + return BCrypt.Net.BCrypt.Verify(password, passHash); + } + } +} \ No newline at end of file diff --git a/Contracts/BindingModels/UserBindingModel.cs b/Contracts/BindingModels/UserBindingModel.cs index 7b6c7c6..d5e19ae 100644 --- a/Contracts/BindingModels/UserBindingModel.cs +++ b/Contracts/BindingModels/UserBindingModel.cs @@ -13,6 +13,7 @@ namespace Contracts.BindingModels public string SecondName { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public string PasswordHash { get; set; } = string.Empty; + public string? Password { get; set; } public DateTime Birthday { get; set; } public RoleBindingModel Role { get; set; } = null!; } diff --git a/Contracts/BusinessLogicContracts/IRoleLogic.cs b/Contracts/BusinessLogicContracts/IRoleLogic.cs new file mode 100644 index 0000000..99e24dc --- /dev/null +++ b/Contracts/BusinessLogicContracts/IRoleLogic.cs @@ -0,0 +1,24 @@ +using Contracts.BindingModels; +using Contracts.SearchModels; +using Contracts.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contracts.BusinessLogicContracts +{ + public interface IRoleLogic + { + RoleViewModel Create(RoleBindingModel model); + + RoleViewModel Update(RoleBindingModel model); + + RoleViewModel ReadElement(RoleSearchModel model); + + IEnumerable ReadElements(RoleSearchModel? model); + + RoleViewModel Delete(RoleSearchModel model); + } +} \ No newline at end of file diff --git a/Contracts/BusinessLogicContracts/IUserLogic.cs b/Contracts/BusinessLogicContracts/IUserLogic.cs new file mode 100644 index 0000000..fdcc510 --- /dev/null +++ b/Contracts/BusinessLogicContracts/IUserLogic.cs @@ -0,0 +1,27 @@ +using Contracts.BindingModels; +using Contracts.SearchModels; +using Contracts.ViewModels; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contracts.BusinessLogicContracts +{ + public interface IUserLogic + { + UserViewModel Login(string email, string password); + + UserViewModel Create(UserBindingModel model); + + UserViewModel Update(UserBindingModel model); + + UserViewModel ReadElement(UserSearchModel model); + + IEnumerable ReadElements(UserSearchModel? model); + + UserViewModel Delete(UserSearchModel model); + } +} \ No newline at end of file diff --git a/Contracts/Contracts.csproj b/Contracts/Contracts.csproj index 64eaa86..fa71b7a 100644 --- a/Contracts/Contracts.csproj +++ b/Contracts/Contracts.csproj @@ -6,9 +6,4 @@ enable - - - - - diff --git a/Contracts/Converters/RoleConverter.cs b/Contracts/Converters/RoleConverter.cs new file mode 100644 index 0000000..37aa7e0 --- /dev/null +++ b/Contracts/Converters/RoleConverter.cs @@ -0,0 +1,25 @@ +using Contracts.BindingModels; +using Contracts.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contracts.Converters +{ + public static class RoleConverter + { + public static RoleViewModel ToView(RoleBindingModel model) => new() + { + Id = model.Id, + Name = model.Name, + }; + + public static RoleBindingModel ToBinding(RoleViewModel model) => new() + { + Id = model.Id, + Name = model.Name, + }; + } +} \ No newline at end of file diff --git a/Contracts/Converters/UserConverter.cs b/Contracts/Converters/UserConverter.cs new file mode 100644 index 0000000..b951d11 --- /dev/null +++ b/Contracts/Converters/UserConverter.cs @@ -0,0 +1,33 @@ +using Contracts.BindingModels; +using Contracts.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contracts.Converters +{ + public static class UserConverter + { + public static UserViewModel ToView(UserBindingModel model) => new() + { + Id = model.Id, + FirstName = model.FirstName, + SecondName = model.SecondName, + Email = model.Email, + Birthday = model.Birthday, + Role = RoleConverter.ToView(model.Role), + }; + + public static UserBindingModel ToBinding(UserViewModel model) => new() + { + Id = model.Id, + FirstName = model.FirstName, + SecondName = model.SecondName, + Email = model.Email, + Birthday = model.Birthday, + Role = RoleConverter.ToBinding(model.Role), + }; + } +} \ No newline at end of file diff --git a/Contracts/Exceptions/AccountException.cs b/Contracts/Exceptions/AccountException.cs new file mode 100644 index 0000000..1678eda --- /dev/null +++ b/Contracts/Exceptions/AccountException.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contracts.Exceptions +{ + public class AccountException : Exception + { + public AccountException() + { + } + + public AccountException(string message) : base(message) + { + } + + public AccountException(string message, Exception inner) : base(message, inner) + { + } + } +} \ No newline at end of file diff --git a/Contracts/Exceptions/ElementNotFoundException.cs b/Contracts/Exceptions/ElementNotFoundException.cs new file mode 100644 index 0000000..7fad056 --- /dev/null +++ b/Contracts/Exceptions/ElementNotFoundException.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contracts.Exceptions +{ + public class ElementNotFoundException : Exception + { + public ElementNotFoundException() : base("Element not founded.") + { + } + + public ElementNotFoundException(string message) : base(message) + { + } + + public ElementNotFoundException(string message, Exception inner) : base(message, inner) + { + } + } +} \ No newline at end of file diff --git a/Contracts/StorageContracts/IRoleStorage.cs b/Contracts/StorageContracts/IRoleStorage.cs new file mode 100644 index 0000000..d8b7f15 --- /dev/null +++ b/Contracts/StorageContracts/IRoleStorage.cs @@ -0,0 +1,24 @@ +using Contracts.BindingModels; +using Contracts.SearchModels; +using Contracts.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contracts.StorageContracts +{ + public interface IRoleStorage + { + RoleBindingModel? Insert(RoleBindingModel model); + + IEnumerable GetList(RoleSearchModel? model); + + RoleBindingModel? GetElement(RoleSearchModel model); + + RoleBindingModel? Update(RoleBindingModel model); + + RoleBindingModel? Delete(RoleSearchModel model); + } +} \ No newline at end of file diff --git a/Contracts/StorageContracts/IUserStorage.cs b/Contracts/StorageContracts/IUserStorage.cs new file mode 100644 index 0000000..3a38799 --- /dev/null +++ b/Contracts/StorageContracts/IUserStorage.cs @@ -0,0 +1,23 @@ +using Contracts.BindingModels; +using Contracts.SearchModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contracts.StorageContracts +{ + public interface IUserStorage + { + UserBindingModel? Insert(UserBindingModel model); + + IEnumerable GetList(UserSearchModel? model); + + UserBindingModel? GetElement(UserSearchModel model); + + UserBindingModel? Update(UserBindingModel model); + + UserBindingModel? Delete(UserSearchModel model); + } +} \ No newline at end of file diff --git a/Contracts/ViewModels/UserViewModel.cs b/Contracts/ViewModels/UserViewModel.cs index 4f15600..6174d56 100644 --- a/Contracts/ViewModels/UserViewModel.cs +++ b/Contracts/ViewModels/UserViewModel.cs @@ -12,7 +12,6 @@ namespace Contracts.ViewModels public string FirstName { get; set; } = string.Empty; public string SecondName { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; - public string PasswordHash { get; set; } = string.Empty; public DateTime Birthday { get; set; } public RoleViewModel Role { get; set; } = null!; } diff --git a/DataModels/Models/IUser.cs b/DataModels/Models/IUser.cs index 7098f65..9eb2ef6 100644 --- a/DataModels/Models/IUser.cs +++ b/DataModels/Models/IUser.cs @@ -13,6 +13,5 @@ namespace DataModels.Models string PasswordHash { get; } string Email { get; } DateTime Birthday { get; } - Guid RoleId { get; } } } \ No newline at end of file diff --git a/DatabaseImplement/Database.cs b/DatabaseImplement/Database.cs new file mode 100644 index 0000000..7bb94c7 --- /dev/null +++ b/DatabaseImplement/Database.cs @@ -0,0 +1,25 @@ +using DatabaseImplement.Models; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DatabaseImplement +{ + public class Database : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (optionsBuilder.IsConfigured == false) + { + optionsBuilder.UseNpgsql("Server=192.168.191.42:32768;Database=gun_market;Username=postgres;Password=7355608;"); + } + base.OnConfiguring(optionsBuilder); + } + + public virtual DbSet Roles { get; set; } = null!; + public virtual DbSet Users { get; set; } = null!; + } +} \ No newline at end of file diff --git a/DatabaseImplement/DatabaseImplement.csproj b/DatabaseImplement/DatabaseImplement.csproj index 85b7ca3..2929944 100644 --- a/DatabaseImplement/DatabaseImplement.csproj +++ b/DatabaseImplement/DatabaseImplement.csproj @@ -7,8 +7,16 @@ - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/DatabaseImplement/Implements/RoleStorage.cs b/DatabaseImplement/Implements/RoleStorage.cs new file mode 100644 index 0000000..4394a59 --- /dev/null +++ b/DatabaseImplement/Implements/RoleStorage.cs @@ -0,0 +1,88 @@ +using Contracts.BindingModels; +using Contracts.SearchModels; +using Contracts.StorageContracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DatabaseImplement.Implements +{ + public class RoleStorage : IRoleStorage + { + public RoleBindingModel? Delete(RoleSearchModel model) + { + if (model.Id is null) + { + return null; + } + + var context = new Database(); + var role = context.Roles.FirstOrDefault(r => r.Id == model.Id); + if (role is null) + { + return null; + } + + context.Remove(role); + context.SaveChanges(); + return role.GetBindingModel(); + } + + public RoleBindingModel? GetElement(RoleSearchModel model) + { + if (model.Id is null) + { + return null; + } + var context = new Database(); + return context.Roles + .FirstOrDefault(r => r.Id == model.Id) + ?.GetBindingModel(); + } + + public IEnumerable GetList(RoleSearchModel? model) + { + var context = new Database(); + if (model is null) + { + return context.Roles.Select(r => r.GetBindingModel()); + } + if (model.Id is null) + { + return []; + } + return context.Roles + .Where(r => r.Id == model.Id) + .Select(r => r.GetBindingModel()); + } + + public RoleBindingModel? Insert(RoleBindingModel model) + { + var context = new Database(); + var newRole = Models.Role.ToRoleFromBinding(model); + + context.Roles.Add(newRole); + context.SaveChanges(); + + return newRole.GetBindingModel(); + } + + public RoleBindingModel? Update(RoleBindingModel model) + { + var context = new Database(); + var role = context.Roles.FirstOrDefault(r => r.Id == model.Id); + + if (role is null) + { + return null; + } + + role.Update(model); + + context.SaveChanges(); + return role.GetBindingModel(); + } + } +} \ No newline at end of file diff --git a/DatabaseImplement/Implements/UserStorage.cs b/DatabaseImplement/Implements/UserStorage.cs new file mode 100644 index 0000000..fdbdf0a --- /dev/null +++ b/DatabaseImplement/Implements/UserStorage.cs @@ -0,0 +1,109 @@ +using Contracts.BindingModels; +using Contracts.SearchModels; +using Contracts.StorageContracts; +using DatabaseImplement.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DatabaseImplement.Implements +{ + public class UserStorage : IUserStorage + { + public UserBindingModel? Delete(UserSearchModel model) + { + if (model.Id is null && model.Email is null) + { + return null; + } + + var context = new Database(); + var user = context.Users.FirstOrDefault(u => + (model.Id.HasValue && u.Id == model.Id) + || (!string.IsNullOrEmpty(u.Email) && u.Email.Contains(model.Email))); + + if (user is null) + { + return null; + } + context.Remove(user); + context.SaveChanges(); + + return user.GetBindingModel(); + } + + public UserBindingModel? GetElement(UserSearchModel model) + { + if (model.Id is null && model.Email is null) + { + return null; + } + var context = new Database(); + return context.Users + .Include(u => u.Role) + .FirstOrDefault(u => + (model.Id.HasValue && u.Id == model.Id) + || (!string.IsNullOrEmpty(u.Email) && u.Email.Contains(model.Email))) + ?.GetBindingModel(); + } + + public IEnumerable GetList(UserSearchModel? model) + { + var context = new Database(); + if (model is null) + { + return context.Users + .Include(u => u.Role) + .Select(r => r.GetBindingModel()); + } + if (model.Id is null && model.Email is null) + { + return []; + } + return context.Users + .Where(u => + (model.Id.HasValue && u.Id == model.Id) + || (!string.IsNullOrEmpty(u.Email) && u.Email.Contains(model.Email))) + .Include(u => u.Role) + .Select(r => r.GetBindingModel()); + } + + public UserBindingModel? Insert(UserBindingModel model) + { + var context = new Database(); + var role = context.Roles.FirstOrDefault(r => r.Id == model.Role.Id); + if (role is null) + { + return null; + } + var newUser = Models.User.ToUserFromBinding(model, role); + + context.Users.Add(newUser); + context.SaveChanges(); + + return newUser.GetBindingModel(); + } + + public UserBindingModel? Update(UserBindingModel model) + { + var context = new Database(); + var user = context.Users + .FirstOrDefault(u => u.Id == model.Id); + var role = context.Roles.FirstOrDefault(r => r.Id == model.Role.Id); + + if (user is null || role is null) + { + return null; + } + + user.Update(model, role); + + context.SaveChanges(); + return user.GetBindingModel(); + } + } +} \ No newline at end of file diff --git a/DatabaseImplement/Migrations/20240604184007_registration_init.Designer.cs b/DatabaseImplement/Migrations/20240604184007_registration_init.Designer.cs new file mode 100644 index 0000000..3dbe69b --- /dev/null +++ b/DatabaseImplement/Migrations/20240604184007_registration_init.Designer.cs @@ -0,0 +1,89 @@ +// +using System; +using DatabaseImplement; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DatabaseImplement.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20240604184007_registration_init")] + partial class registration_init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DatabaseImplement.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("DatabaseImplement.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("SecondName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DatabaseImplement.Models.User", b => + { + b.HasOne("DatabaseImplement.Models.Role", "Role") + .WithMany() + .HasForeignKey("RoleId"); + + b.Navigation("Role"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DatabaseImplement/Migrations/20240604184007_registration_init.cs b/DatabaseImplement/Migrations/20240604184007_registration_init.cs new file mode 100644 index 0000000..8f81df4 --- /dev/null +++ b/DatabaseImplement/Migrations/20240604184007_registration_init.cs @@ -0,0 +1,64 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DatabaseImplement.Migrations +{ + /// + public partial class registration_init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FirstName = table.Column(type: "text", nullable: false), + SecondName = table.Column(type: "text", nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Birthday = table.Column(type: "timestamp with time zone", nullable: false), + RoleId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_RoleId", + table: "Users", + column: "RoleId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Roles"); + } + } +} diff --git a/DatabaseImplement/Migrations/DatabaseModelSnapshot.cs b/DatabaseImplement/Migrations/DatabaseModelSnapshot.cs new file mode 100644 index 0000000..875158d --- /dev/null +++ b/DatabaseImplement/Migrations/DatabaseModelSnapshot.cs @@ -0,0 +1,86 @@ +// +using System; +using DatabaseImplement; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DatabaseImplement.Migrations +{ + [DbContext(typeof(Database))] + partial class DatabaseModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DatabaseImplement.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("DatabaseImplement.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("SecondName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DatabaseImplement.Models.User", b => + { + b.HasOne("DatabaseImplement.Models.Role", "Role") + .WithMany() + .HasForeignKey("RoleId"); + + b.Navigation("Role"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DatabaseImplement/Models/Role.cs b/DatabaseImplement/Models/Role.cs new file mode 100644 index 0000000..f5d7970 --- /dev/null +++ b/DatabaseImplement/Models/Role.cs @@ -0,0 +1,47 @@ +using Contracts.BindingModels; +using Contracts.ViewModels; +using DataModels.Models; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DatabaseImplement.Models +{ + public class Role : IRole + { + public Guid Id { get; set; } + + [Required] + public string Name { get; set; } = string.Empty; + + public RoleBindingModel GetBindingModel() => new() + { + Id = Id, + Name = Name, + }; + + public static Role ToRoleFromView(RoleViewModel model) => new() + { + Id = model.Id, + Name = model.Name, + }; + + public static Role ToRoleFromBinding(RoleBindingModel model) => new() + { + Id = model.Id, + Name = model.Name, + }; + + public void Update(RoleBindingModel model) + { + if (model is null) + { + throw new ArgumentNullException("Update role: bindinng model is null"); + } + Name = model.Name; + } + } +} \ No newline at end of file diff --git a/DatabaseImplement/Models/User.cs b/DatabaseImplement/Models/User.cs new file mode 100644 index 0000000..a2f2ecf --- /dev/null +++ b/DatabaseImplement/Models/User.cs @@ -0,0 +1,82 @@ +using Contracts.BindingModels; +using Contracts.SearchModels; +using Contracts.ViewModels; +using DataModels.Models; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DatabaseImplement.Models +{ + public class User : IUser + { + public Guid Id { get; set; } + + [Required] + public string FirstName { get; set; } = string.Empty; + + [Required] + public string SecondName { get; set; } = string.Empty; + + [Required] + public string PasswordHash { get; set; } = string.Empty; + + [Required] + public string Email { get; set; } = string.Empty; + + [Required] + public DateTime Birthday { get; set; } + + public Role? Role { get; set; } + + public UserBindingModel GetBindingModel() => new() + { + Id = Id, + FirstName = FirstName, + SecondName = SecondName, + Email = Email, + PasswordHash = PasswordHash, + Birthday = Birthday, + Role = Role?.GetBindingModel() ?? new() + }; + + public static User ToUserFromView(UserViewModel model, Role role) => new() + { + Id = model.Id, + FirstName = model.FirstName, + SecondName = model.SecondName, + Email = model.Email, + Birthday = model.Birthday, + Role = role + }; + + public static User ToUserFromBinding(UserBindingModel model, Role role) => new() + { + Id = model.Id, + FirstName = model.FirstName, + SecondName = model.SecondName, + Email = model.Email, + PasswordHash = model.PasswordHash, + Birthday = model.Birthday, + Role = role + }; + + public void Update(UserBindingModel model, Role role) + { + if (model is null) + { + throw new ArgumentNullException("Update user: binding model is null"); + } + + Email = model.Email; + FirstName = model.FirstName; + SecondName = model.SecondName; + PasswordHash = model.PasswordHash; + Birthday = model.Birthday; + Role = role; + } + } +} \ No newline at end of file diff --git a/RestAPI/Controllers/RoleController.cs b/RestAPI/Controllers/RoleController.cs new file mode 100644 index 0000000..9f03335 --- /dev/null +++ b/RestAPI/Controllers/RoleController.cs @@ -0,0 +1,120 @@ +using BusinessLogic.BusinessLogic; +using Contracts.BindingModels; +using Contracts.BusinessLogicContracts; +using Contracts.Exceptions; +using Contracts.SearchModels; +using Contracts.ViewModels; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace RestAPI.Controllers +{ + [Route("[controller]/[action]")] + [ApiController] + public class RoleController + { + private readonly ILogger _logger; + private readonly IRoleLogic _roleLogic; + + public RoleController(ILogger logger, IRoleLogic roleLogic) + { + _logger = logger; + _roleLogic = roleLogic; + } + + [HttpGet] + public IResult Get([FromQuery] RoleSearchModel model) + { + try + { + var res = _roleLogic.ReadElement(model); + return Results.Ok(res); + } + catch (ElementNotFoundException ex) + { + _logger.LogInformation(ex, "Role not found"); + return Results.NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error get role"); + return Results.Problem(ex.Message); + } + } + + [HttpGet] + public IResult GeFilteredtList([FromQuery] RoleSearchModel model) + { + try + { + var res = _roleLogic.ReadElements(model).ToList(); + return Results.Ok(res); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error get list role"); + return Results.Problem(ex.Message); + } + } + + [HttpGet] + public IResult GetList() + { + try + { + var res = _roleLogic.ReadElements(null).ToList(); + return Results.Ok(res); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error get list role"); + return Results.Problem(ex.Message); + } + } + + [HttpPost] + public IResult Create([FromBody] RoleBindingModel model) + { + try + { + var res = _roleLogic.Create(model); + return Results.Created(string.Empty, res); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error create role"); + return Results.Problem(ex.Message); + } + } + + [HttpPatch] + public IResult Update([FromBody] RoleBindingModel model) + { + try + { + var res = _roleLogic.Update(model); + return Results.Ok(res); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error update role"); + return Results.Problem(ex.Message); + } + } + + [HttpDelete] + public IResult Delete([FromQuery] RoleSearchModel model) + { + try + { + var res = _roleLogic.Delete(model); ; + return Results.Ok(res); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error delete role"); + return Results.Problem(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/RestAPI/Controllers/UserController.cs b/RestAPI/Controllers/UserController.cs new file mode 100644 index 0000000..bbeb162 --- /dev/null +++ b/RestAPI/Controllers/UserController.cs @@ -0,0 +1,130 @@ +using BusinessLogic.BusinessLogic; +using Contracts.BindingModels; +using Contracts.BusinessLogicContracts; +using Contracts.Exceptions; +using Contracts.SearchModels; +using Microsoft.AspNetCore.Mvc; + +namespace RestAPI.Controllers +{ + [Route("[controller]/[action]")] + [ApiController] + public class UserController + { + private readonly ILogger _logger; + private readonly IUserLogic _userLogic; + + public UserController(ILogger logger, IUserLogic userLogic) + { + _userLogic = userLogic; + _logger = logger; + } + + [HttpPost] + public IResult Login(string email, string password) + { + try + { + var res = _userLogic.Login(email, password); + return Results.Ok(res); + } + catch (ElementNotFoundException ex) + { + _logger.LogInformation(ex, "User not found"); + return Results.NoContent(); + } + catch (AccountException ex) + { + _logger.LogWarning(ex, "Wrong login data"); + return Results.BadRequest(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error get user"); + return Results.Problem(ex.Message); + } + } + + [HttpPost] + public IResult Registration([FromBody] UserBindingModel model) + { + try + { + var res = _userLogic.Create(model); + return Results.Ok(res); + } + catch (AccountException ex) + { + _logger.LogWarning(ex, "Wrong registration data"); + throw; + return Results.BadRequest(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error create user"); + throw; + return Results.Problem(ex.Message); + } + } + + [HttpGet] + public IResult Get([FromQuery] UserSearchModel model) + { + try + { + var res = _userLogic.ReadElement(model); + return Results.Ok(res); + } + catch (ElementNotFoundException ex) + { + _logger.LogInformation(ex, "User not found"); + return Results.NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error get user"); + return Results.Problem(ex.Message); + } + } + + [HttpPatch] + public IResult Update([FromBody] UserBindingModel model) + { + try + { + var res = _userLogic.Update(model); + return Results.Ok(res); + } + catch (AccountException ex) + { + _logger.LogWarning(ex, "Wrong update data"); + return Results.BadRequest(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error update user"); + return Results.Problem(ex.Message); + } + } + + [HttpDelete] + public IResult Delete(Guid id) + { + try + { + var res = _userLogic.Delete(new() { Id = id }); + return Results.Ok(res); + } + catch (ElementNotFoundException ex) + { + _logger.LogInformation(ex, "User not found"); + return Results.NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error delete user"); + return Results.Problem(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/RestAPI/Controllers/WeatherForecastController.cs b/RestAPI/Controllers/WeatherForecastController.cs deleted file mode 100644 index 40741f2..0000000 --- a/RestAPI/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace RestAPI.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/RestAPI/Program.cs b/RestAPI/Program.cs index 48863a6..6fbbd7c 100644 --- a/RestAPI/Program.cs +++ b/RestAPI/Program.cs @@ -1,19 +1,43 @@ +using BusinessLogic.BusinessLogic; +using Contracts.BusinessLogicContracts; +using Contracts.StorageContracts; +using DatabaseImplement.Implements; +using Microsoft.OpenApi.Models; +using System; + +const string VERSION = "v1"; +const string TITLE = "21GunsRestAPI"; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. +builder.Logging.SetMinimumLevel(LogLevel.Trace); +builder.Logging.AddLog4Net("log4net.config"); + +#region DI + +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +#endregion DI builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc(VERSION, new OpenApiInfo { Title = TITLE, Version = VERSION }); +}); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint($"/swagger/{VERSION}/swagger.json", $"{TITLE} {VERSION}")); } app.UseHttpsRedirection(); @@ -22,4 +46,4 @@ app.UseAuthorization(); app.MapControllers(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/RestAPI/RestAPI.csproj b/RestAPI/RestAPI.csproj index 9daa180..ee6f069 100644 --- a/RestAPI/RestAPI.csproj +++ b/RestAPI/RestAPI.csproj @@ -7,7 +7,24 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + Always + + + diff --git a/RestAPI/RestAPI.http b/RestAPI/RestAPI.http index ae5b1fd..cb9c1f5 100644 --- a/RestAPI/RestAPI.http +++ b/RestAPI/RestAPI.http @@ -1,6 +1,6 @@ @RestAPI_HostAddress = http://localhost:5168 -GET {{RestAPI_HostAddress}}/weatherforecast/ +GET {{RestAPI_HostAddress}}/api/ Accept: application/json ### diff --git a/RestAPI/WeatherForecast.cs b/RestAPI/WeatherForecast.cs deleted file mode 100644 index cf5ac94..0000000 --- a/RestAPI/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace RestAPI -{ - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -} diff --git a/RestAPI/log4net.config b/RestAPI/log4net.config new file mode 100644 index 0000000..0da4e4c --- /dev/null +++ b/RestAPI/log4net.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file