Add project files.

This commit is contained in:
KhasanovAM
2026-01-18 00:30:29 +04:00
parent 9c5a7ce993
commit 61a598aceb
27 changed files with 1974 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Mvc;
using LogisticsApp.Server.Services;
using LogisticsApp.Server.DTOs;
namespace LogisticsApp.Server.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
public AuthController(IAuthService authService)
{
_authService = authService;
}
[HttpPost("login")]
public async Task<ActionResult<ApiResponse<LoginResponse>>> Login(LoginRequest request)
{
if (string.IsNullOrEmpty(request.Username) || string.IsNullOrEmpty(request.Password))
{
return BadRequest(new ApiResponse<string>(false, "Username and password are required"));
}
var result = await _authService.LoginAsync(request);
if (result == null)
{
return Unauthorized(new ApiResponse<string>(false, "Invalid username or password"));
}
Response.Cookies.Append("auth_token", result.Token, new CookieOptions
{
HttpOnly = false,
Secure = false,
SameSite = SameSiteMode.Strict,
Expires = result.Expires
});
return Ok(new ApiResponse<LoginResponse>(true, "Login successful", result));
}
[HttpPost("logout")]
public ActionResult<ApiResponse<string>> Logout()
{
Response.Cookies.Delete("auth_token");
return Ok(new ApiResponse<string>(true, "Logout successful"));
}
}
}

View File

@@ -0,0 +1,205 @@
using LogisticsApp.Server.Data;
using LogisticsApp.Server.DTOs;
using LogisticsApp.Server.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LogisticsApp.Server.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly ApplicationDbContext _context;
public OrdersController(ApplicationDbContext context)
{
_context = context;
}
// GET: api/orders
[Authorize]
[HttpGet]
public async Task<ActionResult<ApiResponse<PagedResult<Order>>>> GetOrders([FromQuery] OrderFilters filters)
{
try
{
var query = _context.Orders.AsQueryable();
if (!string.IsNullOrEmpty(filters.ClientName))
{
query = query.Where(o => o.ClientName.ToLower().Contains(filters.ClientName.ToLower()));
}
if (filters.MinCost.HasValue)
{
query = query.Where(o => o.OrderCost >= filters.MinCost.Value);
}
if (filters.MaxCost.HasValue)
{
query = query.Where(o => o.OrderCost <= filters.MaxCost.Value);
}
if (filters.OrderDate.HasValue)
{
query = query.Where(o => o.OrderDate.Date == filters.OrderDate.Value.Date);
}
if (!string.IsNullOrEmpty(filters.Status))
{
query = query.Where(o => o.Status == filters.Status);
}
var totalCount = await query.CountAsync();
var orders = await query
.OrderByDescending(o => o.OrderDate)
.Skip((filters.Page - 1) * filters.PageSize)
.Take(filters.PageSize)
.ToListAsync();
var result = new PagedResult<Order>
{
Items = orders,
TotalCount = totalCount,
Page = filters.Page,
PageSize = filters.PageSize,
TotalPages = (int)Math.Ceiling(totalCount / (double)filters.PageSize)
};
return Ok(new ApiResponse<PagedResult<Order>>(true, "Orders retrieved successfully", result));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
// GET: api/orders/5
[Authorize]
[HttpGet("{id}")]
public async Task<ActionResult<ApiResponse<Order>>> GetOrder(int id)
{
var order = await _context.Orders.FindAsync(id);
if (order == null)
{
return NotFound(new ApiResponse<string>(false, "Order not found"));
}
return Ok(new ApiResponse<Order>(true, "Order retrieved successfully", order));
}
// PUT: api/orders/5
[Authorize]
[HttpPut("{id}")]
public async Task<ActionResult<ApiResponse<Order>>> PutOrder(int id, Order order)
{
if (id != order.Id)
{
return BadRequest(new ApiResponse<string>(false, "ID mismatch"));
}
var validStatuses = new[] { "pending", "in_progress", "completed", "cancelled" };
if (!validStatuses.Contains(order.Status))
{
return BadRequest(new ApiResponse<string>(false, "Invalid status"));
}
if (order.OrderCost <= 0)
{
return BadRequest(new ApiResponse<string>(false, "Order cost must be positive"));
}
_context.Entry(order).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!OrderExists(id))
{
return NotFound(new ApiResponse<string>(false, "Order not found"));
}
else
{
throw;
}
}
return Ok(new ApiResponse<Order>(true, "Order updated successfully", order));
}
// POST: api/orders
[Authorize]
[HttpPost]
public async Task<ActionResult<ApiResponse<Order>>> PostOrder(Order order)
{
if (string.IsNullOrEmpty(order.ClientName) || order.ClientName.Length > 200)
{
return BadRequest(new ApiResponse<string>(false, "Client name is required and must be less than 200 characters"));
}
if (order.OrderCost <= 0)
{
return BadRequest(new ApiResponse<string>(false, "Order cost must be positive"));
}
if (string.IsNullOrEmpty(order.Address) || order.Address.Length > 500)
{
return BadRequest(new ApiResponse<string>(false, "Address is required and must be less than 500 characters"));
}
var validStatuses = new[] { "pending", "in_progress", "completed", "cancelled" };
if (!validStatuses.Contains(order.Status))
{
return BadRequest(new ApiResponse<string>(false, "Invalid status"));
}
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return CreatedAtAction("GetOrder", new { id = order.Id },
new ApiResponse<Order>(true, "Order created successfully", order));
}
// DELETE: api/orders/5
[Authorize]
[HttpDelete("{id}")]
public async Task<ActionResult<ApiResponse<Order>>> DeleteOrder(int id)
{
try
{
var order = await _context.Orders.FindAsync(id);
if (order == null)
{
return NotFound(new ApiResponse<string>(false, "Order not found"));
}
_context.Orders.Remove(order);
await _context.SaveChangesAsync();
return Ok(new ApiResponse<string>(true, "Order deleted successfully"));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
private bool OrderExists(int id)
{
return _context.Orders.Any(e => e.Id == id);
}
}
}

View File

@@ -0,0 +1,214 @@
using LogisticsApp.Server.Data;
using LogisticsApp.Server.DTOs;
using LogisticsApp.Server.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LogisticsApp.Server.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class VehiclesController : ControllerBase
{
private readonly ApplicationDbContext _context;
public VehiclesController(ApplicationDbContext context)
{
_context = context;
}
// GET: api/vehicles
[Authorize]
[HttpGet]
public async Task<ActionResult<ApiResponse<PagedResult<Vehicle>>>> GetVehicles([FromQuery] VehicleFilters filters)
{
try
{
var query = _context.Vehicles.AsQueryable();
if (!string.IsNullOrEmpty(filters.DriverName))
{
query = query.Where(v => v.DriverName.ToLower().Contains(filters.DriverName.ToLower()));
}
if (!string.IsNullOrEmpty(filters.VehicleType))
{
query = query.Where(v => v.VehicleType.ToLower().Contains(filters.VehicleType.ToLower()));
}
if (!string.IsNullOrEmpty(filters.LicensePlate))
{
query = query.Where(v => v.LicensePlate.ToLower().Contains(filters.LicensePlate.ToLower()));
}
var totalCount = await query.CountAsync();
var vehicles = await query
.OrderBy(v => v.DriverName)
.Skip((filters.Page - 1) * filters.PageSize)
.Take(filters.PageSize)
.ToListAsync();
var result = new PagedResult<Vehicle>
{
Items = vehicles,
TotalCount = totalCount,
Page = filters.Page,
PageSize = filters.PageSize,
TotalPages = (int)Math.Ceiling(totalCount / (double)filters.PageSize)
};
return Ok(new ApiResponse<PagedResult<Vehicle>>(true, "Vehicles retrieved successfully", result));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
// GET: api/vehicles/5
[Authorize]
[HttpGet("{id}")]
public async Task<ActionResult<ApiResponse<Vehicle>>> GetVehicle(int id)
{
try
{
var vehicle = await _context.Vehicles
.FirstOrDefaultAsync(v => v.Id == id);
if (vehicle == null)
{
return NotFound(new ApiResponse<string>(false, "Vehicle not found"));
}
return Ok(new ApiResponse<Vehicle>(true, "Vehicle retrieved successfully", vehicle));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
// POST: api/vehicles
[Authorize]
[HttpPost]
public async Task<ActionResult<ApiResponse<Vehicle>>> PostVehicle(Vehicle vehicle)
{
try
{
if (string.IsNullOrEmpty(vehicle.DriverName) || vehicle.DriverName.Length > 100)
{
return BadRequest(new ApiResponse<string>(false, "Driver name is required and must be less than 100 characters"));
}
if (string.IsNullOrEmpty(vehicle.VehicleType) || vehicle.VehicleType.Length > 50)
{
return BadRequest(new ApiResponse<string>(false, "Vehicle type is required and must be less than 50 characters"));
}
if (string.IsNullOrEmpty(vehicle.LicensePlate) || vehicle.LicensePlate.Length > 20)
{
return BadRequest(new ApiResponse<string>(false, "License plate is required and must be less than 20 characters"));
}
vehicle.CreatedAt = DateTime.UtcNow;
_context.Vehicles.Add(vehicle);
await _context.SaveChangesAsync();
return CreatedAtAction("GetVehicle", new { id = vehicle.Id },
new ApiResponse<Vehicle>(true, "Vehicle created successfully", vehicle));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
// PUT: api/vehicles/5
[Authorize]
[HttpPut("{id}")]
public async Task<ActionResult<ApiResponse<Vehicle>>> PutVehicle(int id, Vehicle vehicle)
{
try
{
if (id != vehicle.Id)
{
return BadRequest(new ApiResponse<string>(false, "ID mismatch"));
}
if (string.IsNullOrEmpty(vehicle.DriverName) || vehicle.DriverName.Length > 100)
{
return BadRequest(new ApiResponse<string>(false, "Driver name is required and must be less than 100 characters"));
}
if (string.IsNullOrEmpty(vehicle.VehicleType) || vehicle.VehicleType.Length > 50)
{
return BadRequest(new ApiResponse<string>(false, "Vehicle type is required and must be less than 50 characters"));
}
if (string.IsNullOrEmpty(vehicle.LicensePlate) || vehicle.LicensePlate.Length > 20)
{
return BadRequest(new ApiResponse<string>(false, "License plate is required and must be less than 20 characters"));
}
_context.Entry(vehicle).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!VehicleExists(id))
{
return NotFound(new ApiResponse<string>(false, "Vehicle not found"));
}
else
{
throw;
}
}
return Ok(new ApiResponse<Vehicle>(true, "Vehicle updated successfully", vehicle));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
// DELETE: api/vehicles/5
[Authorize]
[HttpDelete("{id}")]
public async Task<ActionResult<ApiResponse<string>>> DeleteVehicle(int id)
{
try
{
var vehicle = await _context.Vehicles.FindAsync(id);
if (vehicle == null)
{
return NotFound(new ApiResponse<string>(false, "Vehicle not found"));
}
_context.Vehicles.Remove(vehicle);
await _context.SaveChangesAsync();
return Ok(new ApiResponse<string>(true, "Vehicle deleted successfully"));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
private bool VehicleExists(int id)
{
return _context.Vehicles.Any(e => e.Id == id);
}
}
}

View File

@@ -0,0 +1,231 @@
using LogisticsApp.Server.Data;
using LogisticsApp.Server.DTOs;
using LogisticsApp.Server.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LogisticsApp.Server.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class WaybillEntriesController : ControllerBase
{
private readonly ApplicationDbContext _context;
public WaybillEntriesController(ApplicationDbContext context)
{
_context = context;
}
// GET: api/waybillentries
[Authorize]
[HttpGet]
public async Task<ActionResult<ApiResponse<List<WaybillEntry>>>> GetWaybillEntries()
{
try
{
var entries = await _context.WaybillEntries
.OrderByDescending(w => w.Date)
.ThenBy(w => w.StartTime)
.ToListAsync();
return Ok(new ApiResponse<List<WaybillEntry>>(true, "Waybill entries retrieved successfully", entries));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
// GET: api/waybillentries/5
[Authorize]
[HttpGet("{id}")]
public async Task<ActionResult<ApiResponse<WaybillEntry>>> GetWaybillEntry(int id)
{
try
{
var waybillEntry = await _context.WaybillEntries
.FirstOrDefaultAsync(w => w.Id == id);
if (waybillEntry == null)
{
return NotFound(new ApiResponse<string>(false, "Waybill entry not found"));
}
return Ok(new ApiResponse<WaybillEntry>(true, "Waybill entry retrieved successfully", waybillEntry));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
// POST: api/waybillentries
[Authorize]
[HttpPost]
public async Task<ActionResult<ApiResponse<WaybillEntry>>> PostWaybillEntry(WaybillEntry waybillEntry)
{
try
{
if (waybillEntry.VehicleId <= 0)
{
return BadRequest(new ApiResponse<string>(false, "Valid VehicleId is required"));
}
if (waybillEntry.OrderId <= 0)
{
return BadRequest(new ApiResponse<string>(false, "Valid OrderId is required"));
}
var vehicleExists = await _context.Vehicles.AnyAsync(v => v.Id == waybillEntry.VehicleId);
if (!vehicleExists)
{
return BadRequest(new ApiResponse<string>(false, "Vehicle not found"));
}
var orderExists = await _context.Orders.AnyAsync(o => o.Id == waybillEntry.OrderId);
if (!orderExists)
{
return BadRequest(new ApiResponse<string>(false, "Order not found"));
}
if (string.IsNullOrEmpty(waybillEntry.StartTime) || !IsValidTimeFormat(waybillEntry.StartTime))
{
return BadRequest(new ApiResponse<string>(false, "StartTime must be in HH:mm format"));
}
if (string.IsNullOrEmpty(waybillEntry.EndTime) || !IsValidTimeFormat(waybillEntry.EndTime))
{
return BadRequest(new ApiResponse<string>(false, "EndTime must be in HH:mm format"));
}
waybillEntry.CreatedAt = DateTime.UtcNow;
_context.WaybillEntries.Add(waybillEntry);
await _context.SaveChangesAsync();
var createdEntry = await _context.WaybillEntries
.FirstOrDefaultAsync(w => w.Id == waybillEntry.Id);
return CreatedAtAction("GetWaybillEntry", new { id = waybillEntry.Id },
new ApiResponse<WaybillEntry>(true, "Waybill entry created successfully", createdEntry));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
// PUT: api/waybillentries/5
[Authorize]
[HttpPut("{id}")]
public async Task<ActionResult<ApiResponse<WaybillEntry>>> PutWaybillEntry(int id, WaybillEntry waybillEntry)
{
try
{
if (id != waybillEntry.Id)
{
return BadRequest(new ApiResponse<string>(false, "ID mismatch"));
}
if (waybillEntry.VehicleId <= 0)
{
return BadRequest(new ApiResponse<string>(false, "Valid VehicleId is required"));
}
if (waybillEntry.OrderId <= 0)
{
return BadRequest(new ApiResponse<string>(false, "Valid OrderId is required"));
}
var vehicleExists = await _context.Vehicles.AnyAsync(v => v.Id == waybillEntry.VehicleId);
if (!vehicleExists)
{
return BadRequest(new ApiResponse<string>(false, "Vehicle not found"));
}
var orderExists = await _context.Orders.AnyAsync(o => o.Id == waybillEntry.OrderId);
if (!orderExists)
{
return BadRequest(new ApiResponse<string>(false, "Order not found"));
}
if (string.IsNullOrEmpty(waybillEntry.StartTime) || !IsValidTimeFormat(waybillEntry.StartTime))
{
return BadRequest(new ApiResponse<string>(false, "StartTime must be in HH:mm format"));
}
if (string.IsNullOrEmpty(waybillEntry.EndTime) || !IsValidTimeFormat(waybillEntry.EndTime))
{
return BadRequest(new ApiResponse<string>(false, "EndTime must be in HH:mm format"));
}
_context.Entry(waybillEntry).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!WaybillEntryExists(id))
{
return NotFound(new ApiResponse<string>(false, "Waybill entry not found"));
}
else
{
throw;
}
}
var updatedEntry = await _context.WaybillEntries
.FirstOrDefaultAsync(w => w.Id == waybillEntry.Id);
return Ok(new ApiResponse<WaybillEntry>(true, "Waybill entry updated successfully", updatedEntry));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
// DELETE: api/waybillentries/5
[Authorize]
[HttpDelete("{id}")]
public async Task<ActionResult<ApiResponse<string>>> DeleteWaybillEntry(int id)
{
try
{
var waybillEntry = await _context.WaybillEntries.FindAsync(id);
if (waybillEntry == null)
{
return NotFound(new ApiResponse<string>(false, "Waybill entry not found"));
}
_context.WaybillEntries.Remove(waybillEntry);
await _context.SaveChangesAsync();
return Ok(new ApiResponse<string>(true, "Waybill entry deleted successfully"));
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<string>(false, $"Internal server error: {ex.Message}"));
}
}
private bool WaybillEntryExists(int id)
{
return _context.WaybillEntries.Any(e => e.Id == id);
}
private bool IsValidTimeFormat(string time)
{
return System.Text.RegularExpressions.Regex.IsMatch(time, @"^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$");
}
}
}

26
DTOs/ApiResponse.cs Normal file
View File

@@ -0,0 +1,26 @@
namespace LogisticsApp.Server.DTOs
{
public class ApiResponse<T>
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public T? Data { get; set; }
public ApiResponse(bool success, string message, T? data = default)
{
Success = success;
Message = message;
Data = data;
}
}
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages { get; set; }
}
}

8
DTOs/LoginRequest.cs Normal file
View File

@@ -0,0 +1,8 @@
namespace LogisticsApp.Server.DTOs
{
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
}

10
DTOs/LoginResponse.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace LogisticsApp.Server.DTOs
{
public class LoginResponse
{
public string Token { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
public DateTime Expires { get; set; }
}
}

13
DTOs/OrderFilters.cs Normal file
View File

@@ -0,0 +1,13 @@
namespace LogisticsApp.Server.DTOs
{
public class OrderFilters
{
public string? ClientName { get; set; }
public decimal? MinCost { get; set; }
public decimal? MaxCost { get; set; }
public DateTime? OrderDate { get; set; }
public string? Status { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 50;
}
}

11
DTOs/VehicleFilters.cs Normal file
View File

@@ -0,0 +1,11 @@
namespace LogisticsApp.Server.DTOs
{
public class VehicleFilters
{
public string? DriverName { get; set; }
public string? VehicleType { get; set; }
public string? LicensePlate { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 50;
}
}

View File

@@ -0,0 +1,71 @@
using Microsoft.EntityFrameworkCore;
using LogisticsApp.Server.Models;
namespace LogisticsApp.Server.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<Order> Orders { get; set; }
public DbSet<Vehicle> Vehicles { get; set; }
public DbSet<WaybillEntry> WaybillEntries { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var property in entityType.GetProperties())
{
if (property.ClrType == typeof(DateTime))
{
modelBuilder.Entity(entityType.ClrType)
.Property<DateTime>(property.Name)
.HasConversion(
v => v.Kind == DateTimeKind.Unspecified
? DateTime.SpecifyKind(v, DateTimeKind.Utc)
: v.ToUniversalTime(),
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
}
else if (property.ClrType == typeof(DateTime?))
{
modelBuilder.Entity(entityType.ClrType)
.Property<DateTime?>(property.Name)
.HasConversion(
v => !v.HasValue
? v
: (v.Value.Kind == DateTimeKind.Unspecified
? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc)
: v.Value.ToUniversalTime()),
v => v.HasValue
? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc)
: v);
}
}
}
modelBuilder.Entity<Order>()
.HasIndex(o => o.Status);
modelBuilder.Entity<Order>()
.HasIndex(o => o.OrderDate);
modelBuilder.Entity<Order>()
.HasIndex(o => o.ClientName);
modelBuilder.Entity<Vehicle>()
.HasIndex(v => v.LicensePlate);
modelBuilder.Entity<WaybillEntry>()
.HasIndex(w => new { w.VehicleId, w.Date });
modelBuilder.Entity<User>()
.HasIndex(u => u.Username)
.IsUnique();
base.OnModelCreating(modelBuilder);
}
}
}

82
Data/DbInitializer.cs Normal file
View File

@@ -0,0 +1,82 @@
using LogisticsApp.Server.Models;
namespace LogisticsApp.Server.Data
{
public static class DbInitializer
{
public static async Task Initialize(ApplicationDbContext context)
{
if (context.Users.Any() || context.Orders.Any())
{
return;
}
var user = new User
{
Username = "admin",
PasswordHash = BCrypt.Net.BCrypt.HashPassword("admin123"),
Role = "admin",
CreatedAt = DateTime.UtcNow
};
context.Users.Add(user);
user = new User
{
Username = "user",
PasswordHash = BCrypt.Net.BCrypt.HashPassword("user123"),
Role = "user",
CreatedAt = DateTime.UtcNow
};
context.Users.Add(user);
var orders = new[]
{
new Order { Id = 1, ClientName = "Иванов Иван Иванович", OrderCost = 15000, OrderDate = DateTime.Parse("2025-01-15"), Status = "completed", Address = "ул. Ленина, 10", Description = "Доставка строительных материалов", CreatedAt = DateTime.UtcNow },
new Order { Id = 2, ClientName = "ООО 'СтройМир'", OrderCost = 25000, OrderDate = DateTime.Parse("2025-01-16"), Status = "in_progress", Address = "пр. Мира, 25", Description = "Перевозка оборудования", CreatedAt = DateTime.UtcNow },
new Order { Id = 3, ClientName = "ЗАО 'ТехноПром'", OrderCost = 18000, OrderDate = DateTime.Parse("2025-01-17"), Status = "completed", Address = "ул. Промышленная, 45", Description = "Доставка офисной мебели", CreatedAt = DateTime.UtcNow },
new Order { Id = 4, ClientName = "Сергеев Алексей Петрович", OrderCost = 12000, OrderDate = DateTime.Parse("2025-01-18"), Status = "in_progress", Address = "пр. Космонавтов, 78", Description = "Переезд квартиры", CreatedAt = DateTime.UtcNow },
new Order { Id = 5, ClientName = "ИП Козлова Марина Сергеевна", OrderCost = 22000, OrderDate = DateTime.Parse("2025-01-19"), Status = "in_progress", Address = "ул. Торговая, 12", Description = "Доставка товаров для магазина", CreatedAt = DateTime.UtcNow },
new Order { Id = 6, ClientName = "ООО 'СтройГрад'", OrderCost = 35000, OrderDate = DateTime.Parse("2025-01-20"), Status = "pending", Address = "ул. Строителей, 33", Description = "Перевозка кирпича", CreatedAt = DateTime.UtcNow },
new Order { Id = 7, ClientName = "Федоров Дмитрий Иванович", OrderCost = 8000, OrderDate = DateTime.Parse("2025-01-21"), Status = "completed", Address = "ул. Садовая, 15", Description = "Перевозка бытовой техники", CreatedAt = DateTime.UtcNow },
new Order { Id = 8, ClientName = "ООО 'ПромСнаб'", OrderCost = 28000, OrderDate = DateTime.Parse("2025-01-22"), Status = "cancelled", Address = "пр. Заводской, 67", Description = "Доставка станков", CreatedAt = DateTime.UtcNow },
new Order { Id = 9, ClientName = "Александрова Ольга Викторовна", OrderCost = 9500, OrderDate = DateTime.Parse("2025-01-23"), Status = "cancelled", Address = "ул. Центральная, 89", Description = "Перевозка личных вещей", CreatedAt = DateTime.UtcNow },
new Order { Id = 10, ClientName = "ООО 'ТоргСервис'", OrderCost = 19500, OrderDate = DateTime.Parse("2025-01-24"), Status = "in_progress", Address = "ул. Коммерческая, 56", Description = "Доставка продуктов питания", CreatedAt = DateTime.UtcNow },
new Order { Id = 11, ClientName = "Васильев Павел Сергеевич", OrderCost = 11000, OrderDate = DateTime.Parse("2025-01-25"), Status = "completed", Address = "ул. Молодежная, 23", Description = "Переезд на новую квартиру", CreatedAt = DateTime.UtcNow },
new Order { Id = 12, ClientName = "ЗАО 'ПромИнвест'", OrderCost = 42000, OrderDate = DateTime.Parse("2025-01-26"), Status = "pending", Address = "пр. Индустриальный, 44", Description = "Перевозка промышленного оборудования", CreatedAt = DateTime.UtcNow }
};
context.Orders.AddRange(orders);
var vehicles = new[]
{
new Vehicle { Id = 1, DriverName = "Петров Петр Петрович", VehicleType = "Газель", LicensePlate = "А123БВ77", CreatedAt = DateTime.UtcNow },
new Vehicle { Id = 2, DriverName = "Сидоров Алексей Владимирович", VehicleType = "Камаз", LicensePlate = "В456ГД77", CreatedAt = DateTime.UtcNow },
new Vehicle { Id = 3, DriverName = "Кузнецов Михаил Иванович", VehicleType = "Газель", LicensePlate = "С789ЕЖ77", CreatedAt = DateTime.UtcNow },
new Vehicle { Id = 4, DriverName = "Николаев Андрей Сергеевич", VehicleType = "ЗИЛ", LicensePlate = "Д321ФГ77", CreatedAt = DateTime.UtcNow },
new Vehicle { Id = 5, DriverName = "Морозов Виктор Павлович", VehicleType = "Камаз", LicensePlate = "Е654ХЦ77", CreatedAt = DateTime.UtcNow },
new Vehicle { Id = 6, DriverName = "Орлов Денис Александрович", VehicleType = "Газель", LicensePlate = "Ж987ЧШ77", CreatedAt = DateTime.UtcNow },
new Vehicle { Id = 7, DriverName = "Белов Артем Игоревич", VehicleType = "Фургон", LicensePlate = "З159ЩР77", CreatedAt = DateTime.UtcNow },
new Vehicle { Id = 8, DriverName = "Громов Сергей Викторович", VehicleType = "ЗИЛ", LicensePlate = "И753ЪЫ77", CreatedAt = DateTime.UtcNow }
};
context.Vehicles.AddRange(vehicles);
var waybillEntries = new[]
{
new WaybillEntry { VehicleId = 6, OrderId = 1, StartTime = "20:00", EndTime = "22:00", Date = new DateOnly(2025, 11, 21), CreatedAt = DateTime.UtcNow },
new WaybillEntry { VehicleId = 6, OrderId = 4, StartTime = "14:00", EndTime = "16:00", Date = new DateOnly(2025, 11, 21), CreatedAt = DateTime.UtcNow },
new WaybillEntry { VehicleId = 6, OrderId = 2, StartTime = "10:00", EndTime = "12:00", Date = new DateOnly(2025, 11, 21), CreatedAt = DateTime.UtcNow },
new WaybillEntry { VehicleId = 2, OrderId = 2, StartTime = "20:00", EndTime = "22:00", Date = new DateOnly(2025, 11, 21), CreatedAt = DateTime.UtcNow },
new WaybillEntry { VehicleId = 2, OrderId = 4, StartTime = "08:00", EndTime = "09:00", Date = new DateOnly(2025, 11, 21), CreatedAt = DateTime.UtcNow },
new WaybillEntry { VehicleId = 1, OrderId = 1, StartTime = "08:00", EndTime = "11:00", Date = new DateOnly(2025, 11, 21), CreatedAt = DateTime.UtcNow },
new WaybillEntry { VehicleId = 2, OrderId = 4, StartTime = "08:00", EndTime = "10:00", Date = new DateOnly(2025, 11, 23), CreatedAt = DateTime.UtcNow }
};
context.WaybillEntries.AddRange(waybillEntries);
await context.SaveChangesAsync();
}
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.7.1" />
</ItemGroup>
</Project>

6
LogisticsApp.Server.http Normal file
View File

@@ -0,0 +1,6 @@
@LogisticsApp.Server_HostAddress = http://localhost:5155
GET {{LogisticsApp.Server_HostAddress}}/weatherforecast/
Accept: application/json
###

25
LogisticsApp.Server.sln Normal file
View File

@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36121.58 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogisticsApp.Server", "LogisticsApp.Server.csproj", "{BF240DA8-0F2B-443A-98F6-47565A946F26}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BF240DA8-0F2B-443A-98F6-47565A946F26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF240DA8-0F2B-443A-98F6-47565A946F26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF240DA8-0F2B-443A-98F6-47565A946F26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF240DA8-0F2B-443A-98F6-47565A946F26}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8F81766E-91B3-450A-84EA-45D3E4D33C17}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,60 @@
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
namespace LogisticsApp.Server.Middleware
{
public class JwtMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public JwtMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task Invoke(HttpContext context)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last()
?? context.Request.Cookies["auth_token"];
if (token != null)
{
AttachUserToContext(context, token);
}
await _next(context);
}
private void AttachUserToContext(HttpContext context, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key not configured"));
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = _configuration["Jwt:Issuer"],
ValidAudience = _configuration["Jwt:Audience"],
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "nameid").Value);
context.Items["UserId"] = userId;
context.Items["User"] = jwtToken.Claims.First(x => x.Type == "name").Value;
context.Items["Role"] = jwtToken.Claims.First(x => x.Type == "role").Value;
}
catch { }
}
}
}

203
Migrations/20251128103633_fix.Designer.cs generated Normal file
View File

@@ -0,0 +1,203 @@
// <auto-generated />
using System;
using LogisticsApp.Server.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LogisticsApp.Server.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251128103633_fix")]
partial class fix
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LogisticsApp.Server.Models.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("ClientName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("OrderCost")
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("OrderDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ClientName");
b.HasIndex("OrderDate");
b.HasIndex("Status");
b.ToTable("Orders");
});
modelBuilder.Entity("LogisticsApp.Server.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("LogisticsApp.Server.Models.Vehicle", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DriverName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("LicensePlate")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("VehicleType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("LicensePlate");
b.ToTable("Vehicles");
});
modelBuilder.Entity("LogisticsApp.Server.Models.WaybillEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateOnly>("Date")
.HasColumnType("date");
b.Property<string>("EndTime")
.IsRequired()
.HasMaxLength(5)
.HasColumnType("character varying(5)");
b.Property<int>("OrderId")
.HasColumnType("integer");
b.Property<string>("StartTime")
.IsRequired()
.HasMaxLength(5)
.HasColumnType("character varying(5)");
b.Property<int>("VehicleId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("OrderId");
b.HasIndex("VehicleId", "Date");
b.ToTable("WaybillEntries");
});
modelBuilder.Entity("LogisticsApp.Server.Models.WaybillEntry", b =>
{
b.HasOne("LogisticsApp.Server.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LogisticsApp.Server.Models.Vehicle", "Vehicle")
.WithMany()
.HasForeignKey("VehicleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
b.Navigation("Vehicle");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,150 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LogisticsApp.Server.Migrations
{
/// <inheritdoc />
public partial class fix : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Orders",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ClientName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
OrderCost = table.Column<decimal>(type: "numeric(18,2)", nullable: false),
OrderDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Address = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Orders", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Username = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: false),
Role = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Vehicles",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
DriverName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
VehicleType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
LicensePlate = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Vehicles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "WaybillEntries",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
VehicleId = table.Column<int>(type: "integer", nullable: false),
OrderId = table.Column<int>(type: "integer", nullable: false),
StartTime = table.Column<string>(type: "character varying(5)", maxLength: 5, nullable: false),
EndTime = table.Column<string>(type: "character varying(5)", maxLength: 5, nullable: false),
Date = table.Column<DateOnly>(type: "date", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WaybillEntries", x => x.Id);
table.ForeignKey(
name: "FK_WaybillEntries_Orders_OrderId",
column: x => x.OrderId,
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_WaybillEntries_Vehicles_VehicleId",
column: x => x.VehicleId,
principalTable: "Vehicles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Orders_ClientName",
table: "Orders",
column: "ClientName");
migrationBuilder.CreateIndex(
name: "IX_Orders_OrderDate",
table: "Orders",
column: "OrderDate");
migrationBuilder.CreateIndex(
name: "IX_Orders_Status",
table: "Orders",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_Users_Username",
table: "Users",
column: "Username",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Vehicles_LicensePlate",
table: "Vehicles",
column: "LicensePlate");
migrationBuilder.CreateIndex(
name: "IX_WaybillEntries_OrderId",
table: "WaybillEntries",
column: "OrderId");
migrationBuilder.CreateIndex(
name: "IX_WaybillEntries_VehicleId_Date",
table: "WaybillEntries",
columns: new[] { "VehicleId", "Date" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "WaybillEntries");
migrationBuilder.DropTable(
name: "Orders");
migrationBuilder.DropTable(
name: "Vehicles");
}
}
}

View File

@@ -0,0 +1,200 @@
// <auto-generated />
using System;
using LogisticsApp.Server.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LogisticsApp.Server.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LogisticsApp.Server.Models.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("ClientName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("OrderCost")
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("OrderDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ClientName");
b.HasIndex("OrderDate");
b.HasIndex("Status");
b.ToTable("Orders");
});
modelBuilder.Entity("LogisticsApp.Server.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("LogisticsApp.Server.Models.Vehicle", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DriverName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("LicensePlate")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("VehicleType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("LicensePlate");
b.ToTable("Vehicles");
});
modelBuilder.Entity("LogisticsApp.Server.Models.WaybillEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateOnly>("Date")
.HasColumnType("date");
b.Property<string>("EndTime")
.IsRequired()
.HasMaxLength(5)
.HasColumnType("character varying(5)");
b.Property<int>("OrderId")
.HasColumnType("integer");
b.Property<string>("StartTime")
.IsRequired()
.HasMaxLength(5)
.HasColumnType("character varying(5)");
b.Property<int>("VehicleId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("OrderId");
b.HasIndex("VehicleId", "Date");
b.ToTable("WaybillEntries");
});
modelBuilder.Entity("LogisticsApp.Server.Models.WaybillEntry", b =>
{
b.HasOne("LogisticsApp.Server.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LogisticsApp.Server.Models.Vehicle", "Vehicle")
.WithMany()
.HasForeignKey("VehicleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
b.Navigation("Vehicle");
});
#pragma warning restore 612, 618
}
}
}

35
Models/Orders.cs Normal file
View File

@@ -0,0 +1,35 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace LogisticsApp.Server.Models
{
public class Order
{
[Key]
public int Id { get; set; }
[Required]
[StringLength(200)]
public string ClientName { get; set; } = string.Empty;
[Required]
[Column(TypeName = "decimal(18,2)")]
public decimal OrderCost { get; set; }
[Required]
public DateTime OrderDate { get; set; } = DateTime.UtcNow;
[Required]
[StringLength(20)]
public string Status { get; set; } = "pending"; // pending, in_progress, completed, cancelled
[Required]
[StringLength(500)]
public string Address { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; } = DateTime.UtcNow;
}
}

22
Models/User.cs Normal file
View File

@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace LogisticsApp.Server.Models
{
public class User
{
[Key]
public int Id { get; set; }
[Required]
[StringLength(50)]
public string Username { get; set; } = string.Empty;
[Required]
public string PasswordHash { get; set; } = string.Empty;
[Required]
public string Role { get; set; } = "admin"; // admin, user
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}

24
Models/Vehicle.cs Normal file
View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
namespace LogisticsApp.Server.Models
{
public class Vehicle
{
[Key]
public int Id { get; set; }
[Required]
[StringLength(100)]
public string DriverName { get; set; } = string.Empty;
[Required]
[StringLength(50)]
public string VehicleType { get; set; } = string.Empty;
[Required]
[StringLength(20)]
public string LicensePlate { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}

32
Models/WaybillEntry.cs Normal file
View File

@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace LogisticsApp.Server.Models
{
public class WaybillEntry
{
[Key]
public int Id { get; set; }
[Required]
[ForeignKey("Vehicle")]
public int VehicleId { get; set; }
[Required]
[ForeignKey("Order")]
public int OrderId { get; set; }
[Required]
[StringLength(5)]
public string StartTime { get; set; } = string.Empty; // Format: "HH:mm"
[Required]
[StringLength(5)]
public string EndTime { get; set; } = string.Empty; // Format: "HH:mm"
[Required]
public DateOnly Date { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}

121
Program.cs Normal file
View File

@@ -0,0 +1,121 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Text;
using LogisticsApp.Server.Data;
using LogisticsApp.Server.Services;
using LogisticsApp.Server.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
// Configure Swagger
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Logistics API",
Version = "v1",
Description = "API for Logistics Management System"
});
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
});
// Database Configuration
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// JWT Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key not configured")))
};
});
// Services
builder.Services.AddScoped<IAuthService, AuthService>();
// CORS for React client
builder.Services.AddCors(options =>
{
options.AddPolicy("ReactClient", policy =>
{
policy.WithOrigins("http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.WithExposedHeaders("Set-Cookie");
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors("ReactClient");
app.UseMiddleware<JwtMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// Initialize database with sample data
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.EnsureCreated();
await DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}
app.Run();

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53244",
"sslPort": 44332
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5155",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7071;http://localhost:5155",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

80
Services/AuthService.cs Normal file
View File

@@ -0,0 +1,80 @@
using LogisticsApp.Server.Data;
using LogisticsApp.Server.DTOs;
using LogisticsApp.Server.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace LogisticsApp.Server.Services
{
public interface IAuthService
{
Task<LoginResponse?> LoginAsync(LoginRequest request);
string GenerateJwtToken(User user);
}
public class AuthService : IAuthService
{
private readonly ApplicationDbContext _context;
private readonly IConfiguration _configuration;
public AuthService(ApplicationDbContext context, IConfiguration configuration)
{
_context = context;
_configuration = configuration;
}
public async Task<LoginResponse?> LoginAsync(LoginRequest request)
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Username == request.Username);
if (user == null || !VerifyPassword(request.Password, user.PasswordHash))
{
return null;
}
var token = GenerateJwtToken(user);
return new LoginResponse
{
Token = token,
Username = user.Username,
Role = user.Role,
Expires = DateTime.UtcNow.AddHours(24)
};
}
public string GenerateJwtToken(User user)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key not configured")));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Role, user.Role)
};
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(24),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private static bool VerifyPassword(string password, string passwordHash)
{
return BCrypt.Net.BCrypt.Verify(password, passwordHash);
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

17
appsettings.json Normal file
View File

@@ -0,0 +1,17 @@
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=logisticsdb;Username=postgres;Password=postgres"
},
"Jwt": {
"Key": "your-super-secret-key-at-least-32-characters-long",
"Issuer": "LogisticsApp",
"Audience": "LogisticsAppClient"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}