From 33a8506f9b93423613f70f77825f695df8f56bc3 Mon Sep 17 00:00:00 2001 From: Erman Date: Wed, 9 Sep 2026 05:29:20 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B8=D1=81=D0=BA,=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B0=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B8=D0=B5=D0=BD=D1=82,=20=D0=BF=D0=BE=D1=87=D1=82?= =?UTF-8?q?=D0=B0,=20=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D1=8C=20=D0=BE=D1=82?= =?UTF-8?q?=208=20=D1=81=D0=B8=D0=BC=D0=B2=D0=BE=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEMO/Controllers/AuthController.cs | 45 ++- DEMO/Controllers/CategoriesController.cs | 10 + DEMO/Controllers/OrderItemsController.cs | 12 + DEMO/Controllers/OrdersController.cs | 9 + DEMO/Controllers/ProductsController.cs | 16 ++ DEMO/Controllers/StocksController.cs | 6 + DEMO/Controllers/UsersController.cs | 61 +++- DEMO/Controllers/WarehousesController.cs | 18 ++ DEMO/DemoDB.db-shm | Bin 0 -> 32768 bytes DEMO/DemoDB.db-wal | Bin 0 -> 148352 bytes .../20260909001244_AddUserEmail.Designer.cs | 270 ++++++++++++++++++ .../Migrations/20260909001244_AddUserEmail.cs | 29 ++ DEMO/Migrations/AppDbContextModelSnapshot.cs | 4 + DEMO/Models/User.cs | 2 + DEMO/wwwroot/index.html | 8 + DEMO/wwwroot/script.js | 156 ++++++++-- DEMO/wwwroot/style.css | 9 + 17 files changed, 631 insertions(+), 24 deletions(-) create mode 100644 DEMO/DemoDB.db-shm create mode 100644 DEMO/DemoDB.db-wal create mode 100644 DEMO/Migrations/20260909001244_AddUserEmail.Designer.cs create mode 100644 DEMO/Migrations/20260909001244_AddUserEmail.cs diff --git a/DEMO/Controllers/AuthController.cs b/DEMO/Controllers/AuthController.cs index 1b91a71..dc89330 100644 --- a/DEMO/Controllers/AuthController.cs +++ b/DEMO/Controllers/AuthController.cs @@ -18,6 +18,22 @@ public class AuthController : ControllerBase _context = context; } + // простая проверка почты: есть @ и точка после нее + private static bool IsEmailOk(string email) + { + if (string.IsNullOrWhiteSpace(email)) + return false; // пустая не годится + + int at = email.IndexOf('@'); // ищу собаку + + if (at < 1) + return false; // @ нет или стоит первой + + int dot = email.IndexOf('.', at); // ищу точку после @ + + return dot > at + 1 && dot < email.Length - 1; // точка не сразу и не в конце + } + // войти, адрес POST api/auth/login [HttpPost("login")] public async Task Login(User user) // приходят логин и пароль @@ -38,21 +54,43 @@ public class AuthController : ControllerBase { existingUser.Id, existingUser.Login, + existingUser.Email, Role = existingUser.Role?.Name }); } // регистрация, адрес POST api/auth/register [HttpPost("register")] - public async Task Register(User user) // приходят логин и пароль + public async Task Register(User user) // приходят логин пароль и почта { + // чищу пробелы + user.Login = (user.Login ?? "").Trim(); + user.Email = (user.Email ?? "").Trim(); + + // проверки по порядку, чтоб человек понял что не так + if (user.Login.Length < 2) + return BadRequest("Логин слишком короткий, надо минимум 2 буквы."); + + if (user.Password == null || user.Password.Length < 8) + return BadRequest("Пароль слишком короткий, надо минимум 8 символов."); + + if (!IsEmailOk(user.Email)) + return BadRequest("Почта неправильная, проверьте ее."); + // проверяю нет ли уже такого логина - var exists = await _context.Users + var loginExists = await _context.Users .AnyAsync(u => u.Login == user.Login); - if (exists) + if (loginExists) return BadRequest("Такой логин уже занят."); + // проверяю не занята ли почта + var emailExists = await _context.Users + .AnyAsync(u => u.Email == user.Email); + + if (emailExists) + return BadRequest("Такая почта уже зарегистрирована."); + // ищу роль клиент чтоб дать ее новичку var clientRole = await _context.Roles .FirstOrDefaultAsync(r => r.Name == "Client"); @@ -70,6 +108,7 @@ public class AuthController : ControllerBase { user.Id, user.Login, + user.Email, Role = clientRole.Name }); } diff --git a/DEMO/Controllers/CategoriesController.cs b/DEMO/Controllers/CategoriesController.cs index 051da9c..dcc3972 100644 --- a/DEMO/Controllers/CategoriesController.cs +++ b/DEMO/Controllers/CategoriesController.cs @@ -30,6 +30,11 @@ public class CategoriesController : ControllerBase [HttpPost] public async Task CreateCategory(Category category) // приходит название { + category.Name = (category.Name ?? "").Trim(); // убираю пробелы + + if (category.Name.Length < 2) + return BadRequest("Название слишком короткое, надо минимум 2 буквы."); + _context.Categories.Add(category); // добавляю await _context.SaveChangesAsync(); // сохраняю return Ok(category); @@ -44,6 +49,11 @@ public class CategoriesController : ControllerBase if (existingCategory == null) return NotFound(); // нет такой + category.Name = (category.Name ?? "").Trim(); + + if (category.Name.Length < 2) + return BadRequest("Название слишком короткое, надо минимум 2 буквы."); + existingCategory.Name = category.Name; // меняю название await _context.SaveChangesAsync(); // сохраняю diff --git a/DEMO/Controllers/OrderItemsController.cs b/DEMO/Controllers/OrderItemsController.cs index 610713e..052e2bb 100644 --- a/DEMO/Controllers/OrderItemsController.cs +++ b/DEMO/Controllers/OrderItemsController.cs @@ -35,6 +35,12 @@ public class OrderItemsController : ControllerBase [HttpPost] public async Task CreateOrderItem(OrderItem orderItem) // приходит заказ товар колво цена { + if (orderItem.Quantity <= 0) + return BadRequest("Количество должно быть больше нуля."); + + if (orderItem.Price <= 0) + return BadRequest("Цена должна быть больше нуля."); + _context.OrderItems.Add(orderItem); // добавляю await _context.SaveChangesAsync(); // сохраняю return Ok(orderItem); @@ -49,6 +55,12 @@ public class OrderItemsController : ControllerBase if (existingItem == null) return NotFound(); + if (orderItem.Quantity <= 0) + return BadRequest("Количество должно быть больше нуля."); + + if (orderItem.Price <= 0) + return BadRequest("Цена должна быть больше нуля."); + // меняю поля existingItem.OrderId = orderItem.OrderId; existingItem.ProductId = orderItem.ProductId; diff --git a/DEMO/Controllers/OrdersController.cs b/DEMO/Controllers/OrdersController.cs index ea8a57e..ae26b7d 100644 --- a/DEMO/Controllers/OrdersController.cs +++ b/DEMO/Controllers/OrdersController.cs @@ -30,10 +30,16 @@ public class OrdersController : ControllerBase return Ok(orders); } + // какие статусы можно ставить + private static readonly string[] OkStatuses = ["New", "Processing", "Completed", "Cancelled"]; + // добавить заказ [HttpPost] public async Task CreateOrder(Order order) // приходит человек дата статус { + if (!OkStatuses.Contains(order.Status)) + return BadRequest("Такого статуса нет."); + _context.Orders.Add(order); // добавляю await _context.SaveChangesAsync(); // сохраняю return Ok(order); @@ -48,6 +54,9 @@ public class OrdersController : ControllerBase if (existingOrder == null) return NotFound(); + if (!OkStatuses.Contains(order.Status)) + return BadRequest("Такого статуса нет."); + // меняю поля existingOrder.UserId = order.UserId; existingOrder.Date = order.Date; diff --git a/DEMO/Controllers/ProductsController.cs b/DEMO/Controllers/ProductsController.cs index 18caa5e..d0530ff 100644 --- a/DEMO/Controllers/ProductsController.cs +++ b/DEMO/Controllers/ProductsController.cs @@ -34,6 +34,14 @@ public class ProductsController : ControllerBase [HttpPost] public async Task CreateProduct(Product product) // приходит товар { + product.Name = (product.Name ?? "").Trim(); // убираю пробелы + + if (product.Name.Length < 2) + return BadRequest("Название слишком короткое, надо минимум 2 буквы."); + + if (product.Price <= 0) + return BadRequest("Цена должна быть больше нуля."); + // проверяю есть ли такая категория var categoryExists = await _context.Categories .AnyAsync(c => c.Id == product.CategoryId); @@ -57,6 +65,14 @@ public class ProductsController : ControllerBase if (existingProduct == null) return NotFound(); // нет такого + product.Name = (product.Name ?? "").Trim(); + + if (product.Name.Length < 2) + return BadRequest("Название слишком короткое, надо минимум 2 буквы."); + + if (product.Price <= 0) + return BadRequest("Цена должна быть больше нуля."); + // меняю поля existingProduct.Name = product.Name; existingProduct.Price = product.Price; diff --git a/DEMO/Controllers/StocksController.cs b/DEMO/Controllers/StocksController.cs index 0c3998b..0d1d95f 100644 --- a/DEMO/Controllers/StocksController.cs +++ b/DEMO/Controllers/StocksController.cs @@ -35,6 +35,9 @@ public class StocksController : ControllerBase [HttpPost] public async Task CreateStock(Stock stock) // приходит товар склад и количество { + if (stock.Quantity < 0) + return BadRequest("Количество не может быть меньше нуля."); + _context.Stocks.Add(stock); // добавляю await _context.SaveChangesAsync(); // сохраняю return Ok(stock); @@ -49,6 +52,9 @@ public class StocksController : ControllerBase if (existingStock == null) return NotFound(); + if (stock.Quantity < 0) + return BadRequest("Количество не может быть меньше нуля."); + // меняю поля existingStock.ProductId = stock.ProductId; existingStock.WarehouseId = stock.WarehouseId; diff --git a/DEMO/Controllers/UsersController.cs b/DEMO/Controllers/UsersController.cs index b8939e5..0d6dd81 100644 --- a/DEMO/Controllers/UsersController.cs +++ b/DEMO/Controllers/UsersController.cs @@ -18,6 +18,54 @@ public class UsersController : ControllerBase _context = context; } + // простая проверка почты: есть @ и точка после нее + private static bool IsEmailOk(string email) + { + if (string.IsNullOrWhiteSpace(email)) + return false; // пустая не годится + + int at = email.IndexOf('@'); // ищу собаку + + if (at < 1) + return false; // @ нет или стоит первой + + int dot = email.IndexOf('.', at); // ищу точку после @ + + return dot > at + 1 && dot < email.Length - 1; // точка не сразу и не в конце + } + + // проверяю человека перед сохранением, возвращаю текст ошибки или пусто + private async Task CheckUser(User user, int skipId) + { + user.Login = (user.Login ?? "").Trim(); // убираю пробелы + user.Email = (user.Email ?? "").Trim(); + + if (user.Login.Length < 2) + return "Логин слишком короткий, надо минимум 2 буквы."; + + if (user.Password == null || user.Password.Length < 8) + return "Пароль слишком короткий, надо минимум 8 символов."; + + if (!IsEmailOk(user.Email)) + return "Почта неправильная, проверьте ее."; + + // логин должен быть только у одного (себя не считаю) + bool loginBusy = await _context.Users + .AnyAsync(u => u.Login == user.Login && u.Id != skipId); + + if (loginBusy) + return "Такой логин уже занят."; + + // почта тоже только у одного + bool emailBusy = await _context.Users + .AnyAsync(u => u.Email == user.Email && u.Id != skipId); + + if (emailBusy) + return "Такая почта уже зарегистрирована."; + + return ""; // все хорошо + } + // отдать всех людей [HttpGet] public async Task GetUsers() @@ -32,8 +80,13 @@ public class UsersController : ControllerBase // добавить человека [HttpPost] - public async Task CreateUser(User user) // приходит логин пароль роль + public async Task CreateUser(User user) // приходит логин пароль почта роль { + string error = await CheckUser(user, 0); // проверяю, 0 значит нового + + if (error != "") + return BadRequest(error); // что-то не так, говорю что + _context.Users.Add(user); // добавляю await _context.SaveChangesAsync(); // сохраняю return Ok(user); @@ -48,9 +101,15 @@ public class UsersController : ControllerBase if (existingUser == null) return NotFound(); + string error = await CheckUser(user, id); // проверяю, себя пропускаю + + if (error != "") + return BadRequest(error); + // меняю поля existingUser.Login = user.Login; existingUser.Password = user.Password; + existingUser.Email = user.Email; existingUser.RoleId = user.RoleId; await _context.SaveChangesAsync(); // сохраняю diff --git a/DEMO/Controllers/WarehousesController.cs b/DEMO/Controllers/WarehousesController.cs index 2c727ca..dc35738 100644 --- a/DEMO/Controllers/WarehousesController.cs +++ b/DEMO/Controllers/WarehousesController.cs @@ -30,6 +30,15 @@ public class WarehousesController : ControllerBase [HttpPost] public async Task CreateWarehouse(Warehouse warehouse) // приходит название и адрес { + warehouse.Name = (warehouse.Name ?? "").Trim(); + warehouse.Address = (warehouse.Address ?? "").Trim(); + + if (warehouse.Name.Length < 2) + return BadRequest("Название склада слишком короткое."); + + if (warehouse.Address.Length < 3) + return BadRequest("Адрес слишком короткий."); + _context.Warehouses.Add(warehouse); // добавляю await _context.SaveChangesAsync(); // сохраняю return Ok(warehouse); @@ -44,6 +53,15 @@ public class WarehousesController : ControllerBase if (existingWarehouse == null) return NotFound(); + warehouse.Name = (warehouse.Name ?? "").Trim(); + warehouse.Address = (warehouse.Address ?? "").Trim(); + + if (warehouse.Name.Length < 2) + return BadRequest("Название склада слишком короткое."); + + if (warehouse.Address.Length < 3) + return BadRequest("Адрес слишком короткий."); + // меняю поля existingWarehouse.Name = warehouse.Name; existingWarehouse.Address = warehouse.Address; diff --git a/DEMO/DemoDB.db-shm b/DEMO/DemoDB.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..c68bfc7dc05647d0eae4a867a13748ade4cf52a2 GIT binary patch literal 32768 zcmeI*KWh7kxdTly6WPcO zMG8kxns20^{GMg)?tJcNCx70xtVfYvYkSl9JdAw1{`LFf#L${+NBa{QP15 zeL9~_?)m#z>aKF#o0VF8-A)z{v3J{8B%kv;Bzsz2EB9KiwcIz~bG`hnB0zuu0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UcnE=B%$V3} zuO4D6dl2ZDz(!12Ic{&;X~)jxI0V)&(2p5HN9*6l4g@+Vuo=^{UU%?ZjznPn0y*VP z5vVCJXm8qC&DM2YUSRld=4g2e0ObU7My?`IQy`~9D*`nIM(w<20yPEl zE(}GWra;~TqX^U#$or2Jftmtt^Q!s3>pBIVV<1pgU|hCyjT3l*fIwM+N!iXdPT*zB N8@Ck!0tEgqa0_q3D4YNQ literal 0 HcmV?d00001 diff --git a/DEMO/DemoDB.db-wal b/DEMO/DemoDB.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..c8d577e7c12eca919bb822b774246849a55e142d GIT binary patch literal 148352 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 J;TOAo{|7=#lZ5~P literal 0 HcmV?d00001 diff --git a/DEMO/Migrations/20260909001244_AddUserEmail.Designer.cs b/DEMO/Migrations/20260909001244_AddUserEmail.Designer.cs new file mode 100644 index 0000000..1b995e5 --- /dev/null +++ b/DEMO/Migrations/20260909001244_AddUserEmail.Designer.cs @@ -0,0 +1,270 @@ +// +using System; +using DEMO.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DEMO.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260909001244_AddUserEmail")] + partial class AddUserEmail + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); + + modelBuilder.Entity("DEMO.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("DEMO.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("DEMO.Models.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("OrderId") + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("DEMO.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("WarehouseId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("DEMO.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Login") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DEMO.Models.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("DEMO.Models.Order", b => + { + b.HasOne("DEMO.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DEMO.Models.OrderItem", b => + { + b.HasOne("DEMO.Models.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DEMO.Models.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("DEMO.Models.Product", b => + { + b.HasOne("DEMO.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("DEMO.Models.Stock", b => + { + b.HasOne("DEMO.Models.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DEMO.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("DEMO.Models.User", b => + { + b.HasOne("DEMO.Models.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DEMO/Migrations/20260909001244_AddUserEmail.cs b/DEMO/Migrations/20260909001244_AddUserEmail.cs new file mode 100644 index 0000000..54ad2c0 --- /dev/null +++ b/DEMO/Migrations/20260909001244_AddUserEmail.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DEMO.Migrations +{ + /// + public partial class AddUserEmail : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Email", + table: "Users", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Email", + table: "Users"); + } + } +} diff --git a/DEMO/Migrations/AppDbContextModelSnapshot.cs b/DEMO/Migrations/AppDbContextModelSnapshot.cs index 7824653..c238f8f 100644 --- a/DEMO/Migrations/AppDbContextModelSnapshot.cs +++ b/DEMO/Migrations/AppDbContextModelSnapshot.cs @@ -150,6 +150,10 @@ namespace DEMO.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("Login") .IsRequired() .HasColumnType("TEXT"); diff --git a/DEMO/Models/User.cs b/DEMO/Models/User.cs index 6c286b0..98797ba 100644 --- a/DEMO/Models/User.cs +++ b/DEMO/Models/User.cs @@ -9,6 +9,8 @@ public class User public string Password { get; set; } = string.Empty; // пароль + public string Email { get; set; } = string.Empty; // почта + public int RoleId { get; set; } // какая роль (номер) public Role? Role { get; set; } // сама роль (подтянется) diff --git a/DEMO/wwwroot/index.html b/DEMO/wwwroot/index.html index 756b233..54c48a7 100644 --- a/DEMO/wwwroot/index.html +++ b/DEMO/wwwroot/index.html @@ -72,6 +72,11 @@ + +
@@ -128,6 +133,9 @@

Управление товарами магазина

+ + + diff --git a/DEMO/wwwroot/script.js b/DEMO/wwwroot/script.js index 3d03d28..c4078f8 100644 --- a/DEMO/wwwroot/script.js +++ b/DEMO/wwwroot/script.js @@ -29,6 +29,7 @@ const pageTitle = document.getElementById("pageTitle"); const pageDescription = document.getElementById("pageDescription"); const pageContent = document.getElementById("pageContent"); const addButton = document.getElementById("addButton"); +const searchInput = document.getElementById("searchInput"); // поле поиска // всплывающее окно const modal = document.getElementById("modal"); @@ -55,6 +56,29 @@ let currentPage = "products"; let editingId = null; // кто вошел в систему let currentAuthUser = null; +// я админ или нет (у админа больше кнопок) +let isAdmin = false; +// данные текущей страницы, чтоб искать по ним +let currentData = []; + +// проверяю админ ли я, если нет то ругаюсь и не пускаю +function needAdmin() { + if (isAdmin) { + return true; + } + showToast("Только админ может это делать."); + return false; +} + +// проверяю почту: есть @ и точка после нее +function isEmailOk(email) { + const at = email.indexOf("@"); // ищу собаку + if (at < 1) { + return false; + } + const dot = email.indexOf(".", at); // ищу точку после @ + return dot > at + 1 && dot < email.length - 1; +} // показываю сообщение снизу на 2 секунды function showToast(message) { @@ -145,10 +169,13 @@ function showRegisterForm() { // захожу в программу после входа function enterApplication(user) { currentAuthUser = user; // запоминаю кто вошел + isAdmin = user.role === "Admin"; // проверяю админ ли я sessionStorage.setItem("demoUser", JSON.stringify(user)); // сохраняю чтоб не выкидывало currentUser.textContent = user.login; // пишу логин currentRole.textContent = user.role || "Client"; // пишу роль userAvatar.textContent = user.login ? user.login.charAt(0).toUpperCase() : "U"; // первая буква в кружок + // кнопку Пользователи показываю только админу + document.querySelector('.nav-button[data-page="users"]').classList.toggle("hidden", !isAdmin); authScreen.classList.add("hidden"); // прячу вход app.classList.remove("hidden"); // показываю программу showPage("products"); // открываю товары @@ -194,8 +221,9 @@ registerForm.addEventListener("submit", async (event) => { const login = document.getElementById("registerLogin").value.trim(); const password = document.getElementById("registerPassword").value; const passwordRepeat = document.getElementById("registerPasswordRepeat").value; + const email = document.getElementById("registerEmail").value.trim(); // простые проверки - if (!login || !password || !passwordRepeat) { + if (!login || !password || !passwordRepeat || !email) { registerError.textContent = "Заполните все поля."; return; } @@ -203,8 +231,12 @@ registerForm.addEventListener("submit", async (event) => { registerError.textContent = "Логин слишком короткий."; return; } - if (password.length < 4) { - registerError.textContent = "Пароль слишком короткий."; + if (password.length < 8) { + registerError.textContent = "Пароль должен быть минимум 8 символов."; + return; + } + if (!isEmailOk(email)) { + registerError.textContent = "Почта неправильная, проверьте ее."; return; } if (password !== passwordRepeat) { @@ -214,7 +246,7 @@ registerForm.addEventListener("submit", async (event) => { try { await api("/api/auth/register", { method: "POST", - body: JSON.stringify({ login, password, roleId: 2 }) + body: JSON.stringify({ login, password, email, roleId: 2 }) }); registerForm.reset(); showLoginForm(); // иду на вход @@ -242,6 +274,13 @@ document.querySelectorAll(".nav-button").forEach((button) => { // открываю страницу (товары, заказы и тд) async function showPage(page) { + // в пользователи пускаю только админа + if (page === "users" && !isAdmin) { + pageTitle.textContent = "Пользователи"; + pageDescription.textContent = "Все люди в системе"; + pageContent.innerHTML = "
Нет правТолько админ видит эту страницу
"; + return; + } currentPage = page; // запоминаю где я const config = pages[page]; if (!config) { @@ -249,15 +288,35 @@ async function showPage(page) { } pageTitle.textContent = config.title; // пишу название pageDescription.textContent = config.description; + // кнопку добавить показываю админу везде, а обычному только в заказах + addButton.classList.toggle("hidden", !(isAdmin || page === "orders")); + searchInput.value = ""; // чищу поиск pageContent.innerHTML = "
Загрузка...
"; // пока грузится try { const data = await api(config.endpoint); // беру данные с сервера + currentData = data; // запоминаю чтоб искать по ним renderPage(page, data); // рисую } catch (error) { pageContent.innerHTML = "
Не получилось: " + escapeHtml(error.message) + "
"; } } +// ищу по странице когда печатаю в поиск +searchInput.addEventListener("input", () => { + const q = searchInput.value.trim().toLowerCase(); // что ищу + if (!q) { + renderPage(currentPage, currentData); // пусто показываю все + return; + } + // ищу совпадение где угодно (название, логин, адрес и тд) + const found = currentData.filter((item) => JSON.stringify(item).toLowerCase().includes(q)); + if (found.length === 0) { + pageContent.innerHTML = "
Ничего не нашлось
"; + return; + } + renderPage(currentPage, found); // рисую найденное +}); + // выбираю что рисовать function renderPage(page, data) { // если пусто то так и пишу @@ -294,10 +353,14 @@ function renderProducts(products) { html += "

" + escapeHtml(p.name) + "

"; html += "
" + formatMoney(p.price) + "
"; html += "
Категория: " + escapeHtml(p.category ? p.category.name : "—") + "
"; - html += "
"; - html += ""; - html += ""; - html += "
"; + // кнопки показываю только админу + if (isAdmin) { + html += "
"; + html += ""; + html += ""; + html += "
"; + } + html += ""; } html += ""; pageContent.innerHTML = html; // показываю все @@ -308,8 +371,11 @@ function renderCategories(categories) { let html = "
"; for (const c of categories) { html += ""; } html += "
IDНазваниеДействия
#" + c.id + "" + escapeHtml(c.name) + "
"; - html += ""; - html += ""; + // кнопки только для админа + if (isAdmin) { + html += ""; + html += ""; + } html += "
"; @@ -321,8 +387,11 @@ function renderWarehouses(warehouses) { let html = "
"; for (const w of warehouses) { html += ""; } html += "
IDНазваниеАдресДействия
#" + w.id + "" + escapeHtml(w.name) + "" + escapeHtml(w.address) + "
"; - html += ""; - html += ""; + // кнопки только для админа + if (isAdmin) { + html += ""; + html += ""; + } html += "
"; @@ -336,8 +405,11 @@ function renderStock(stocks) { const pname = s.product ? s.product.name : "—"; const wname = s.warehouse ? s.warehouse.name : "—"; html += "#" + s.id + "" + escapeHtml(pname) + "" + escapeHtml(wname) + "" + s.quantity + "
"; - html += ""; - html += ""; + // кнопки только для админа + if (isAdmin) { + html += ""; + html += ""; + } html += "
"; } html += ""; @@ -346,10 +418,15 @@ function renderStock(stocks) { // рисую пользователей function renderUsers(users) { - let html = "
"; + // сюда пускаю только админа (на всякий случай) + if (!isAdmin) { + pageContent.innerHTML = "
Нет прав
"; + return; + } + let html = "
IDЛогинРольДействия
"; for (const u of users) { const rname = u.role ? u.role.name : "—"; - html += ""; @@ -365,8 +442,11 @@ function renderOrders(orders) { const uname = o.user ? o.user.login : "—"; html += ""; } html += "
IDЛогинПочтаРольДействия
#" + u.id + "" + escapeHtml(u.login) + "" + escapeHtml(rname) + "
"; + html += "
#" + u.id + "" + escapeHtml(u.login) + "" + escapeHtml(u.email || "—") + "" + escapeHtml(rname) + "
"; html += ""; html += ""; html += "
#" + o.id + "" + escapeHtml(uname) + "" + formatDate(o.date) + "" + escapeHtml(o.status) + "
"; html += ""; - html += ""; - html += ""; + // менять и удалять заказы может только админ, смотреть товары могут все + if (isAdmin) { + html += ""; + html += ""; + } html += "
"; @@ -376,6 +456,10 @@ function renderOrders(orders) { // кнопка Добавить сверху addButton.addEventListener("click", () => { editingId = null; // значит добавляю а не изменяю + // заказы создавать можно всем, остальное только админу + if (currentPage !== "orders" && !needAdmin()) { + return; + } if (currentPage === "products") { openProductForm(); } @@ -466,6 +550,9 @@ async function saveProduct() { // жму изменить на товаре async function editProduct(id) { + if (!needAdmin()) { // только админ + return; + } const products = await api("/api/products"); // беру все const product = products.find((x) => x.id === id); // ищу нужный if (!product) { @@ -511,6 +598,9 @@ async function saveCategory() { // жму изменить на категории async function editCategory(id) { + if (!needAdmin()) { // только админ + return; + } const categories = await api("/api/categories"); const category = categories.find((x) => x.id === id); if (!category) { @@ -560,6 +650,9 @@ async function saveWarehouse() { // жму изменить на складе async function editWarehouse(id) { + if (!needAdmin()) { // только админ + return; + } const warehouses = await api("/api/warehouses"); const warehouse = warehouses.find((x) => x.id === id); if (!warehouse) { @@ -622,6 +715,9 @@ async function saveStock() { // жму изменить на остатке async function editStock(id) { + if (!needAdmin()) { // только админ + return; + } const stocks = await api("/api/stocks"); const stock = stocks.find((x) => x.id === id); if (!stock) { @@ -642,10 +738,12 @@ async function openUserForm(user = null) { const title = user ? "Изменить пользователя" : "Добавить пользователя"; const login = user ? escapeHtml(user.login) : ""; const password = user ? escapeHtml(user.password) : ""; + const email = user ? escapeHtml(user.email || "") : ""; openModal(title, "
" + "" + "" + + "" + "" + "
" + "
" @@ -656,13 +754,22 @@ async function openUserForm(user = null) { async function saveUser() { const login = document.getElementById("userLogin").value.trim(); const password = document.getElementById("userPassword").value; + const email = document.getElementById("userEmail").value.trim(); const roleId = Number(document.getElementById("userRole").value); - if (!login || !password) { + if (!login || !password || !email) { showToast("Заполните все поля."); return; } + if (password.length < 8) { + showToast("Пароль должен быть минимум 8 символов."); + return; + } + if (!isEmailOk(email)) { + showToast("Почта неправильная, проверьте ее."); + return; + } try { - const data = { login, password, roleId }; + const data = { login, password, email, roleId }; if (editingId) { await api("/api/users/" + editingId, { method: "PUT", body: JSON.stringify(data) }); showToast("Пользователь изменен."); @@ -679,6 +786,9 @@ async function saveUser() { // жму изменить на пользователе async function editUser(id) { + if (!needAdmin()) { // только админ + return; + } const users = await api("/api/users"); const user = users.find((x) => x.id === id); if (!user) { @@ -743,6 +853,9 @@ async function saveOrder() { // жму изменить на заказе async function editOrder(id) { + if (!needAdmin()) { // только админ + return; + } const orders = await api("/api/orders"); const order = orders.find((x) => x.id === id); if (!order) { @@ -820,6 +933,9 @@ async function saveOrderItem(orderId) { // удаляю что угодно (товар, заказ и тд) async function deleteItem(type, id) { + if (!needAdmin()) { // удалять может только админ + return; + } // красивые названия для вопроса const names = { products: "товар", diff --git a/DEMO/wwwroot/style.css b/DEMO/wwwroot/style.css index 0c4236e..44b2715 100644 --- a/DEMO/wwwroot/style.css +++ b/DEMO/wwwroot/style.css @@ -235,6 +235,15 @@ button { margin-top: 5px; } +/* поле поиска */ +.search-input { + height: 44px; + padding: 0 12px; + border: 1px solid #ccc; + border-radius: 8px; + width: 220px; +} + /* синяя кнопка добавить */ .primary-button { padding: 10px 16px;