Add project files.
This commit is contained in:
52
Controllers/AuthController.cs
Normal file
52
Controllers/AuthController.cs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
Controllers/OrdersController.cs
Normal file
205
Controllers/OrdersController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
214
Controllers/VehiclesController.cs
Normal file
214
Controllers/VehiclesController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
231
Controllers/WaybillEntriesController.cs
Normal file
231
Controllers/WaybillEntriesController.cs
Normal 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
26
DTOs/ApiResponse.cs
Normal 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
8
DTOs/LoginRequest.cs
Normal 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
10
DTOs/LoginResponse.cs
Normal 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
13
DTOs/OrderFilters.cs
Normal 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
11
DTOs/VehicleFilters.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Data/ApplicationDbContext.cs
Normal file
71
Data/ApplicationDbContext.cs
Normal 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
82
Data/DbInitializer.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
LogisticsApp.Server.csproj
Normal file
27
LogisticsApp.Server.csproj
Normal 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
6
LogisticsApp.Server.http
Normal 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
25
LogisticsApp.Server.sln
Normal 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
|
||||||
60
Middleware/JwtMiddleware.cs
Normal file
60
Middleware/JwtMiddleware.cs
Normal 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
203
Migrations/20251128103633_fix.Designer.cs
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
Migrations/20251128103633_fix.cs
Normal file
150
Migrations/20251128103633_fix.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
200
Migrations/ApplicationDbContextModelSnapshot.cs
Normal file
200
Migrations/ApplicationDbContextModelSnapshot.cs
Normal 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
35
Models/Orders.cs
Normal 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
22
Models/User.cs
Normal 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
24
Models/Vehicle.cs
Normal 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
32
Models/WaybillEntry.cs
Normal 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
121
Program.cs
Normal 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();
|
||||||
41
Properties/launchSettings.json
Normal file
41
Properties/launchSettings.json
Normal 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
80
Services/AuthService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
appsettings.Development.json
Normal file
8
appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
appsettings.json
Normal file
17
appsettings.json
Normal 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": "*"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user