Edicion y agregacion de direcciones

This commit is contained in:
devRaGonSa 2025-04-28 21:42:35 +02:00
parent 49df540747
commit 065effae3d
36 changed files with 2132 additions and 96 deletions

View File

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35327.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebVentaCoche", "WebVentaCoche\WebVentaCoche.csproj", "{95AAD8ED-D4F9-4972-81EB-36FCD0FA2C7D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebVentaCoche", "WebVentaCoche\WebVentaCoche.csproj", "{95AAD8ED-D4F9-4972-81EB-36FCD0FA2C7D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View File

@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using System.Threading.Tasks;
using WebVentaCoche.DataBase;
using WebVentaCoche.Helpers;
using WebVentaCoche.ViewModels;
using Microsoft.EntityFrameworkCore;
namespace WebVentaCoche.Controllers
{
[Authorize]
public class AccountController : Controller
{
private readonly IUserHelper _userHelper;
private readonly ApplicationDbContext _context;
public AccountController(IUserHelper userHelper, ApplicationDbContext context)
{
_userHelper = userHelper;
_context = context;
}
//GET:/Account/Settings
[HttpGet]
public async Task<IActionResult> Settings()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userHelper.GetUserByIdAsync(userId);
if (user == null) return NotFound();
var addresses = await _context.Addresses
.Where(a => a.UserId == userId)
.ToListAsync();
var vm = new UserDetailsViewModel
{
Id = user.Id,
Name = user.Name,
Surname = user.Surname,
Email = user.Email,
PhoneNumber = user.PhoneNumber,
UserType = user.UserType,
Addresses = addresses.Select(a => new AddressViewModel
{
Id = a.Id,
Street = a.Street,
City = a.City,
State = a.State,
ZipCode = a.ZipCode,
Country = a.Country
}).ToList()
};
return View(vm);
}
//GET:/Account/Addresses
[HttpGet]
public async Task<IActionResult> Addresses()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var addresses = await _context.Addresses.Where(a => a.UserId == userId).ToListAsync();
var vm = addresses.Select(a => new AddressViewModel
{
Id = a.Id,
Street = a.Street,
City = a.City,
State = a.State,
ZipCode = a.ZipCode,
Country = a.Country
}).ToList();
return View(vm);
}
//GET:/Account/Security
[HttpGet]
public IActionResult Security()
{
//TODO:VM con políticas de contraseña, etc.
return View();
}
}
}

View File

@ -0,0 +1,83 @@
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WebVentaCoche.DataBase;
using WebVentaCoche.Models;
using WebVentaCoche.ViewModels;
namespace WebVentaCoche.Controllers
{
[Authorize]
public class AddressController : Controller
{
private readonly ApplicationDbContext _context;
public AddressController(ApplicationDbContext context)
{
_context = context;
}
//POST:/Address/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(AddressViewModel input)
{
if (!ModelState.IsValid)
return RedirectToAction("Addresses", "Account");
var entity = new Address
{
Street = input.Street,
City = input.City,
State = input.State,
ZipCode = input.ZipCode,
Country = input.Country,
UserId = User.FindFirstValue(ClaimTypes.NameIdentifier)!
};
_context.Addresses.Add(entity);
await _context.SaveChangesAsync();
return RedirectToAction("Addresses", "Account");
}
//POST:/Address/Edit/{id}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, Address model)
{
if (id != model.Id)
return BadRequest();
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var address = await _context.Addresses.FindAsync(id);
if (address == null || address.UserId != userId)
return NotFound();
address.Street = model.Street;
address.City = model.City;
address.State = model.State;
address.ZipCode = model.ZipCode;
address.Country = model.Country;
await _context.SaveChangesAsync();
return RedirectToAction("Addresses", "Account");
}
//POST: /Address/Delete/{id}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var address = await _context.Addresses.FindAsync(id);
if (address == null || address.UserId != userId)
return NotFound();
_context.Addresses.Remove(address);
await _context.SaveChangesAsync();
return RedirectToAction("Addresses", "Account");
}
}
}

View File

@ -0,0 +1,94 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WebVentaCoche.DataBase;
using WebVentaCoche.Helpers;
using WebVentaCoche.Models;
using System.Linq;
namespace WebVentaCoche.Controllers
{
public class CartController : Controller
{
private readonly ApplicationDbContext _context;
public CartController(ApplicationDbContext context)
{
_context = context;
}
// GET: /Cart/Index
// Muestra la lista de productos del carrito y el total.
public IActionResult Index()
{
// Diccionario de IDs -> Cantidades desde sesión (o cualquier fuente)
var itemsDict = CartSessionHelper.GetCartItems(HttpContext.Session);
// Crear tu ViewModel
var viewModel = new CartViewModel();
foreach (var kvp in itemsDict)
{
var productId = kvp.Key;
var amount = kvp.Value;
var product = _context.Products.Find(productId);
if (product != null)
{
viewModel.Products.Add(new CartProduct
{
Product = product,
Amount = amount
});
}
}
return View(viewModel);
}
// POST: /Cart/AddProductToCart
// Añade el producto al carrito y devuelve un JSON con el nuevo contador
[HttpPost]
public IActionResult AddProductToCart(int id)
{
var product = _context.Products.Find(id);
if (product == null)
{
return NotFound();
}
// Añade a la sesión
CartSessionHelper.AddToCart(HttpContext.Session, id);
// Devuelve la cuenta actualizada para actualizar el badge
var productIds = CartSessionHelper.GetCartItems(HttpContext.Session);
int count = productIds.Count;
return Json(new { success = true, cartCount = count });
}
// POST: /Cart/Remove
// Elimina un producto según su ID
[HttpPost]
public IActionResult Remove(int id)
{
CartSessionHelper.RemoveFromCart(HttpContext.Session, id);
return RedirectToAction("Index");
}
// POST: /Cart/Clear
// Vacía todo el carrito (la sesión)
[HttpPost]
public IActionResult Clear()
{
CartSessionHelper.ClearCart(HttpContext.Session);
return RedirectToAction("Index");
}
// GET: /Cart/Checkout
// (Opcional) Muestra un formulario o datos para la compra/pago.
public IActionResult Checkout()
{
return View();
}
}
}

View File

@ -1,6 +1,9 @@
// Controllers/HomeController.cs
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using WebVentaCoche.DataBase;
using WebVentaCoche.Models;
@ -11,45 +14,41 @@ namespace WebVentaCoche.Controllers
private readonly ILogger<HomeController> _logger;
private readonly ApplicationDbContext _context;
public HomeController(ILogger<HomeController> logger, ApplicationDbContext context)
{
_logger = logger;
_context = context;
}
public IActionResult Index()
{
return View();
}
public IActionResult Index() => View();
public IActionResult Privacy()
{
return View();
}
public IActionResult Privacy() => View();
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
=> View(new ErrorViewModel
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier
});
//GET:/Home/ProductsHome
public async Task<IActionResult> ProductsHome()
{
var products = await _context.Products.ToListAsync();
var products = await _context.Products.AsNoTracking().ToListAsync();
return View(products);
}
//GET:/Home/ProductsDetailsHome/5
public async Task<IActionResult> ProductsDetailsHome(int id)
{
if (id <= 0)
return BadRequest();
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
return View(product);
}
}
}

View File

@ -45,7 +45,7 @@ namespace WebVentaCoche.Controllers
return View(order);
}
// GET: Order/Edit/5
//GET:Order/Edit/{id}
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
@ -62,7 +62,7 @@ namespace WebVentaCoche.Controllers
return View(order);
}
// POST: Order/Edit/5
//POST:Order/Edit/{id}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, Order order)
@ -97,7 +97,6 @@ namespace WebVentaCoche.Controllers
return View(order);
}
// Método auxiliar para verificar si la orden existe
private bool OrderExists(int id)
{
return _context.Orders.Any(e => e.Id == id);

View File

@ -14,7 +14,6 @@ namespace WebVentaCoche.Controllers
_context = context;
}
// Lista de productos paginada
public async Task<IActionResult> Index(int page = 1, int pageSize = 10)
{
var products = await _context.Products
@ -28,7 +27,6 @@ namespace WebVentaCoche.Controllers
return View(products);
}
// Ver detalles de un producto
public async Task<IActionResult> Details(int id)
{
var product = await _context.Products.FindAsync(id);
@ -39,14 +37,12 @@ namespace WebVentaCoche.Controllers
return View(product);
}
// Página para añadir producto
[HttpGet]
public IActionResult Create()
{
return View();
}
// Procesar el POST para añadir producto
[HttpPost]
public async Task<IActionResult> Create(Product product, IFormFile Image)
{

View File

@ -225,5 +225,8 @@ namespace WebVentaCoche.Controllers
};
}
}
}

View File

@ -15,5 +15,17 @@ namespace WebVentaCoche.DataBase
public DbSet<User> Users { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderDetail> OrderDetails { get; set; }
public DbSet<Address> Addresses { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Address>()
.HasOne(a => a.User)
.WithMany(u => u.Addresses)
.HasForeignKey(a => a.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}

View File

@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Http;
using System.Text.Json;
namespace WebVentaCoche.Helpers
{
public static class CartSessionHelper
{
private const string CartSessionKey = "CartItems";
//Obtiene el diccionario: ProductId -> Quantity
public static Dictionary<int, int> GetCartItems(ISession session)
{
var json = session.GetString(CartSessionKey);
if (string.IsNullOrEmpty(json))
return new Dictionary<int, int>();
return JsonSerializer.Deserialize<Dictionary<int, int>>(json)
?? new Dictionary<int, int>();
}
//Guarda el diccionario en la sesión
private static void SaveCartItems(ISession session, Dictionary<int, int> items)
{
var json = JsonSerializer.Serialize(items);
session.SetString(CartSessionKey, json);
}
//Añade un producto al carrito
public static void AddToCart(ISession session, int productId)
{
var items = GetCartItems(session);
if (items.ContainsKey(productId))
items[productId]++;
else
items[productId] = 1;
SaveCartItems(session, items);
}
//Actualiza la cantidad de un producto. Si la cantidad es 0 o menor, lo elimina
public static void UpdateQuantity(ISession session, int productId, int quantity)
{
var items = GetCartItems(session);
if (items.ContainsKey(productId))
{
if (quantity <= 0)
{
items.Remove(productId);
}
else
{
items[productId] = quantity;
}
SaveCartItems(session, items);
}
}
//Elimina un producto del carrito
public static void RemoveFromCart(ISession session, int productId)
{
var items = GetCartItems(session);
if (items.ContainsKey(productId))
{
items.Remove(productId);
}
SaveCartItems(session, items);
}
//Vacía el carrito entero
public static void ClearCart(ISession session)
{
session.Remove(CartSessionKey);
}
}
}

View File

@ -16,5 +16,6 @@ namespace WebVentaCoche.Helpers
Task<string> GenerateEmailConfirmationTokenAsync(User user);
Task<IdentityResult> ConfirmEmailAsync(User user, string token);
Task<bool> IsEmailConfirmedAsync(User user);
Task<User?> GetUserByIdAsync(string id);
}
}

View File

@ -0,0 +1,18 @@
using AutoMapper;
using WebVentaCoche.Models;
using WebVentaCoche.ViewModels;
namespace WebVentaCoche.Helpers
{
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<User, UserDetailsViewModel>()
.ForMember(dest => dest.Addresses,
opt => opt.MapFrom(src => src.Addresses));
CreateMap<Address, AddressViewModel>();
}
}
}

View File

@ -75,5 +75,10 @@ namespace WebVentaCoche.Helpers
{
return await _userManager.IsEmailConfirmedAsync(user);
}
public async Task<User?> GetUserByIdAsync(string id)
{
return await _userManager.FindByIdAsync(id);
}
}
}

View File

@ -0,0 +1,418 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WebVentaCoche.DataBase;
#nullable disable
namespace WebVentaCoche.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250424122629_ChangePriceToDecimal")]
partial class ChangePriceToDecimal
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("WebVentaCoche.Models.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("OrderDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("ShippedDate")
.HasColumnType("datetime2");
b.Property<string>("ShippingAddress")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<decimal>("TotalAmount")
.HasColumnType("decimal(18,2)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Orders");
});
modelBuilder.Entity("WebVentaCoche.Models.OrderDetail", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<int>("ProductId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<decimal>("UnitPrice")
.HasColumnType("decimal(18,2)");
b.HasKey("Id");
b.HasIndex("OrderId");
b.HasIndex("ProductId");
b.ToTable("OrderDetails");
});
modelBuilder.Entity("WebVentaCoche.Models.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ImagePath")
.HasColumnType("nvarchar(max)");
b.Property<string>("LongDescription")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<decimal>("Price")
.HasColumnType("decimal(18,2)");
b.Property<string>("ShortDescription")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Products");
});
modelBuilder.Entity("WebVentaCoche.Models.User", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<string>("Surname")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<int>("UserType")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("WebVentaCoche.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("WebVentaCoche.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("WebVentaCoche.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("WebVentaCoche.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("WebVentaCoche.Models.Order", b =>
{
b.HasOne("WebVentaCoche.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("WebVentaCoche.Models.OrderDetail", b =>
{
b.HasOne("WebVentaCoche.Models.Order", "Order")
.WithMany("OrderDetails")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("WebVentaCoche.Models.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
b.Navigation("Product");
});
modelBuilder.Entity("WebVentaCoche.Models.Order", b =>
{
b.Navigation("OrderDetails");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace WebVentaCoche.Migrations
{
/// <inheritdoc />
public partial class ChangePriceToDecimal : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -0,0 +1,478 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WebVentaCoche.DataBase;
#nullable disable
namespace WebVentaCoche.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250428182320_AddAddressEntity")]
partial class AddAddressEntity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("WebVentaCoche.Models.Address", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("City")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Country")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("State")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Street")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("ZipCode")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Addresses");
});
modelBuilder.Entity("WebVentaCoche.Models.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("OrderDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("ShippedDate")
.HasColumnType("datetime2");
b.Property<string>("ShippingAddress")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<decimal>("TotalAmount")
.HasColumnType("decimal(18,2)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Orders");
});
modelBuilder.Entity("WebVentaCoche.Models.OrderDetail", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("OrderId")
.HasColumnType("int");
b.Property<int>("ProductId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<decimal>("UnitPrice")
.HasColumnType("decimal(18,2)");
b.HasKey("Id");
b.HasIndex("OrderId");
b.HasIndex("ProductId");
b.ToTable("OrderDetails");
});
modelBuilder.Entity("WebVentaCoche.Models.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ImagePath")
.HasColumnType("nvarchar(max)");
b.Property<string>("LongDescription")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<decimal>("Price")
.HasColumnType("decimal(18,2)");
b.Property<string>("ShortDescription")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Products");
});
modelBuilder.Entity("WebVentaCoche.Models.User", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<string>("Surname")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<int>("UserType")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("WebVentaCoche.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("WebVentaCoche.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("WebVentaCoche.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("WebVentaCoche.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("WebVentaCoche.Models.Address", b =>
{
b.HasOne("WebVentaCoche.Models.User", "User")
.WithMany("Addresses")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("WebVentaCoche.Models.Order", b =>
{
b.HasOne("WebVentaCoche.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("WebVentaCoche.Models.OrderDetail", b =>
{
b.HasOne("WebVentaCoche.Models.Order", "Order")
.WithMany("OrderDetails")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("WebVentaCoche.Models.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
b.Navigation("Product");
});
modelBuilder.Entity("WebVentaCoche.Models.Order", b =>
{
b.Navigation("OrderDetails");
});
modelBuilder.Entity("WebVentaCoche.Models.User", b =>
{
b.Navigation("Addresses");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace WebVentaCoche.Migrations
{
/// <inheritdoc />
public partial class AddAddressEntity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Addresses",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Street = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
City = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
State = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
ZipCode = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
Country = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Addresses", x => x.Id);
table.ForeignKey(
name: "FK_Addresses_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Addresses_UserId",
table: "Addresses",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Addresses");
}
}
}

View File

@ -155,6 +155,50 @@ namespace WebVentaCoche.Migrations
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("WebVentaCoche.Models.Address", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("City")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Country")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("State")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Street")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("ZipCode")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Addresses");
});
modelBuilder.Entity("WebVentaCoche.Models.Order", b =>
{
b.Property<int>("Id")
@ -375,6 +419,17 @@ namespace WebVentaCoche.Migrations
.IsRequired();
});
modelBuilder.Entity("WebVentaCoche.Models.Address", b =>
{
b.HasOne("WebVentaCoche.Models.User", "User")
.WithMany("Addresses")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("WebVentaCoche.Models.Order", b =>
{
b.HasOne("WebVentaCoche.Models.User", "User")
@ -409,6 +464,11 @@ namespace WebVentaCoche.Migrations
{
b.Navigation("OrderDetails");
});
modelBuilder.Entity("WebVentaCoche.Models.User", b =>
{
b.Navigation("Addresses");
});
#pragma warning restore 612, 618
}
}

View File

@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WebVentaCoche.Models
{
public class Address
{
[Key]
public int Id { get; set; }
[Required, StringLength(100)]
public string Street { get; set; }
[Required, StringLength(50)]
public string City { get; set; }
[StringLength(50)]
public string State { get; set; }
[Required, StringLength(20)]
public string ZipCode { get; set; }
[Required, StringLength(50)]
public string Country { get; set; }
[Required]
public string UserId { get; set; }
[ForeignKey(nameof(UserId))]
public User User { get; set; }
}
}

View File

@ -0,0 +1,19 @@
namespace WebVentaCoche.Models
{
public class Cart
{
public int Id { get; set; }
public string? UserId { get; set; }
public List<Product> Products { get; set; } = new List<Product>();
public decimal Total
{
get
{
return Products.Sum(p => p.Price);
}
}
}
}

View File

@ -0,0 +1,9 @@
namespace WebVentaCoche.Models
{
public class CartProduct
{
public Product Product { get; set; }
public int Amount { get; set; }
public decimal Subtotal => Product.Price * Amount;
}
}

View File

@ -0,0 +1,8 @@
namespace WebVentaCoche.Models
{
public class CartViewModel
{
public List<CartProduct> Products { get; set; } = new List<CartProduct>();
public decimal Total => Products.Sum(p => p.Subtotal);
}
}

View File

@ -8,5 +8,6 @@ namespace WebVentaCoche.Models
public string Name { get; set; }
public string Surname { get; set; }
public UserType UserType { get; set; }
public ICollection<Address> Addresses { get; set; } = new List<Address>();
}
}

View File

@ -1,8 +1,10 @@
// Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using AutoMapper;
using WebVentaCoche.DataBase;
using WebVentaCoche.Helpers;
using WebVentaCoche.Models;
@ -10,77 +12,95 @@ using WebVentaCoche.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
//Configuración de Entity Framework + SQL Server
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
.EnableSensitiveDataLogging()
);
builder.Services.AddScoped<IUserHelper, UserHelper>();
builder.Services.AddTransient<SeedDb>();
builder.Services.AddIdentity<User, IdentityRole>(x =>
//Identity + JWT + Helpers
builder.Services.AddIdentity<User, IdentityRole>(opts =>
{
x.User.RequireUniqueEmail = true;
x.Password.RequireDigit = false;
x.Password.RequiredUniqueChars = 0;
x.Password.RequireLowercase = false;
x.Password.RequireNonAlphanumeric = false;
x.Password.RequireUppercase = false;
opts.User.RequireUniqueEmail = true;
opts.Password.RequireDigit = false;
opts.Password.RequireLowercase = false;
opts.Password.RequireNonAlphanumeric = false;
opts.Password.RequireUppercase = false;
opts.Password.RequiredUniqueChars = 0;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x => x.TokenValidationParameters = new TokenValidationParameters
.AddJwtBearer(opts =>
{
opts.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:SecretToken"]!)),
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["JWT:SecretToken"]!)
),
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddScoped<IUserHelper, UserHelper>();
builder.Services.AddScoped<VerificationService>();
builder.Services.AddTransient<SeedDb>();
builder.Services.AddTransient<EmailService>();
//MVC + Session + Memoria
builder.Services.AddControllersWithViews();
builder.Services.AddMemoryCache();
builder.Services.AddSession(opts =>
{
opts.IdleTimeout = TimeSpan.FromMinutes(30);
opts.Cookie.HttpOnly = true;
// opts.Cookie.SecurePolicy = CookieSecurePolicy.Always; //produccion
});
builder.Services.AddAutoMapper(cfg =>
{
cfg.AddProfile<MappingProfile>();
});
var app = builder.Build();
SeedData(app);
void SeedData(WebApplication app)
//Pipeline de middleware
if (app.Environment.IsDevelopment())
{
IServiceScopeFactory? scopedFactory = app.Services.GetService<IServiceScopeFactory>();
using (IServiceScope? scope = scopedFactory!.CreateScope())
{
SeedDb? service = scope.ServiceProvider.GetService<SeedDb>();
service!.SeedAsync().Wait();
// En dev vemos la excepción completa y stack-trace en el navegador
app.UseDeveloperExceptionPage();
}
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
pattern: "{controller=Home}/{action=Index}/{id?}"
);
// 5. (Opcional) Seed de datos
using (var scope = app.Services.CreateScope())
{
var seeder = scope.ServiceProvider.GetRequiredService<SeedDb>();
seeder.SeedAsync().Wait();
}
app.Run();

View File

@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
namespace WebVentaCoche.ViewModels
{
public class SecurityViewModel
{
[Required]
[DataType(DataType.Password)]
[Display(Name = "Contraseña actual")]
public string CurrentPassword { get; set; } = null!;
[Required]
[StringLength(100, ErrorMessage = "La {0} debe tener al menos {2} y como máximo {1} caracteres.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Nueva contraseña")]
public string NewPassword { get; set; } = null!;
[Required]
[DataType(DataType.Password)]
[Display(Name = "Confirmar nueva contraseña")]
[Compare("NewPassword", ErrorMessage = "La nueva contraseña y la confirmación no coinciden.")]
public string ConfirmNewPassword { get; set; } = null!;
[Display(Name = "Autenticación multifactor habilitada")]
public bool Is2faEnabled { get; set; }
//Opcional: codigo de verificación para MFA
[DataType(DataType.Text)]
[Display(Name = "Código de verificación MFA")]
public string? TwoFactorCode { get; set; }
}
}

View File

@ -0,0 +1,37 @@
using System.Collections.Generic;
using WebVentaCoche.Enums;
namespace WebVentaCoche.ViewModels
{
public class UserDetailsViewModel
{
public string Id { get; set; } = null!;
public string Name { get; set; } = null!;
public string Surname { get; set; } = null!;
public string Email { get; set; } = null!;
public string PhoneNumber { get; set; } = null!;
public UserType UserType { get; set; }
public List<AddressViewModel> Addresses { get; set; } = new();
}
public class AddressViewModel
{
public int? Id { get; set; }
public string Street { get; set; } = null!;
public string City { get; set; } = null!;
public string State { get; set; } = null!;
public string ZipCode { get; set; } = null!;
public string Country { get; set; } = null!;
}
}

View File

@ -0,0 +1,130 @@
@model IEnumerable<WebVentaCoche.ViewModels.AddressViewModel>
@{
Layout = "_AccountLayout";
var addresses = Model.ToList();
}
<h2 class="mb-4">Mis Direcciones</h2>
@if (!addresses.Any())
{
<p>No tienes direcciones registradas.</p>
}
<div class="mb-4">
<button class="btn btn-success btn-sm"
data-bs-toggle="modal"
data-bs-target="#addressModal"
data-id="">
<i class="fa fa-plus me-1"></i>
@(!addresses.Any() ? "Añadir dirección" : "Agregar dirección")
</button>
</div>
@if (addresses.Any())
{
<div class="row">
@foreach (var addr in addresses)
{
<div class="col-md-6 mb-4">
<div class="card p-3" style="background-color: rgba(255,255,255,0.8); border-radius:10px;">
<p class="mb-3">
<strong>@addr.Street</strong><br />
@addr.City, @addr.State @addr.ZipCode<br />
@addr.Country
</p>
<button class="btn btn-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#addressModal"
data-id="@addr.Id"
data-street="@addr.Street"
data-city="@addr.City"
data-state="@addr.State"
data-zipcode="@addr.ZipCode"
data-country="@addr.Country">
<i class="fa fa-pencil-alt me-1"></i> Editar
</button>
</div>
</div>
}
</div>
}
<div class="modal fade" id="addressModal" tabindex="-1" aria-labelledby="addressModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form id="addressForm" method="post">
@Html.AntiForgeryToken()
<div class="modal-header">
<h5 class="modal-title" id="addressModalLabel">Dirección</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
<input type="hidden" id="AddressId" name="Id" />
<div class="mb-3">
<label for="Street" class="form-label">Calle</label>
<input type="text" class="form-control" id="Street" name="Street" required />
</div>
<div class="mb-3">
<label for="City" class="form-label">Ciudad</label>
<input type="text" class="form-control" id="City" name="City" required />
</div>
<div class="mb-3">
<label for="State" class="form-label">Provincia / Estado</label>
<input type="text" class="form-control" id="State" name="State" />
</div>
<div class="mb-3">
<label for="ZipCode" class="form-label">Código Postal</label>
<input type="text" class="form-control" id="ZipCode" name="ZipCode" required />
</div>
<div class="mb-3">
<label for="Country" class="form-label">País</label>
<input type="text" class="form-control" id="Country" name="Country" required />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary btn-sm">Guardar</button>
</div>
</form>
</div>
</div>
</div>
@section Scripts {
<script>
//Al cargar la página, mueve la <div id="addressModal"> al final de <body>
document.addEventListener('DOMContentLoaded', () => {
const modalEl = document.getElementById('addressModal');
if (modalEl.parentNode !== document.body) {
document.body.appendChild(modalEl);
}
});
document.getElementById('addressModal')
.addEventListener('show.bs.modal', event => {
const button = event.relatedTarget;
const form = document.getElementById('addressForm');
const title = document.getElementById('addressModalLabel');
const id = button.getAttribute('data-id');
if (id) {
title.textContent = 'Modificar Dirección';
form.action = '/Address/Edit/' + id;
document.getElementById('AddressId').value = id;
document.getElementById('Street').value = button.dataset.street;
document.getElementById('City').value = button.dataset.city;
document.getElementById('State').value = button.dataset.state;
document.getElementById('ZipCode').value = button.dataset.zipcode;
document.getElementById('Country').value = button.dataset.country;
} else {
title.textContent = 'Añadir Dirección';
form.action = '/Address/Create';
document.getElementById('AddressId').value = '';
form.reset();
}
});
</script>
}

View File

@ -0,0 +1,48 @@
@model WebVentaCoche.ViewModels.UserDetailsViewModel
@{
ViewData["Title"] = "Detalles de Cuenta";
}
<div class="container">
<h2 class="text-center my-4">Detalles del Usuario</h2>
<div class="row justify-content-center mb-4">
<div class="col-md-8">
<div class="card p-3" style="background-color: rgba(255,255,255,0.8); border-radius:10px;">
<h5>@Model.Name @Model.Surname</h5>
<dl class="row">
<dt class="col-sm-4">Email</dt>
<dd class="col-sm-8">@Model.Email</dd>
<dt class="col-sm-4">Teléfono</dt>
<dd class="col-sm-8">@Model.PhoneNumber</dd>
<dt class="col-sm-4">Tipo</dt>
<dd class="col-sm-8">@Model.UserType</dd>
</dl>
</div>
</div>
</div>
<h3 class="text-center mb-3">Direcciones</h3>
@if (Model.Addresses.Any())
{
<div class="row justify-content-center">
<div class="col-md-8">
@foreach (var addr in Model.Addresses)
{
<div class="card mb-3 p-3" style="background: rgba(255,255,255,0.8); border-radius:10px;">
<p>
<strong>@addr.Street</strong><br />
@addr.City, @addr.State @addr.ZipCode<br />
@addr.Country
</p>
</div>
}
</div>
</div>
}
else
{
<p class="text-center">No has agregado ninguna dirección todavía.</p>
}
</div>

View File

@ -0,0 +1,8 @@
@model WebVentaCoche.ViewModels.SecurityViewModel
@{
Layout = "_AccountLayout";
}
<h2>Seguridad de la Cuenta</h2>
<p>Aquí podrás cambiar tu contraseña, configurar MFA, etc.</p>
<!-- tu formulario de cambio de contraseña y las teselas de MFA -->

View File

@ -0,0 +1,16 @@
@model WebVentaCoche.ViewModels.UserDetailsViewModel
@{
Layout = "_AccountLayout";
}
<h2>Mi Cuenta</h2>
<dl class="row">
<dt class="col-sm-3">Nombre</dt>
<dd class="col-sm-9">@Model.Name @Model.Surname</dd>
<dt class="col-sm-3">Email</dt>
<dd class="col-sm-9">@Model.Email</dd>
<dt class="col-sm-3">Teléfono</dt>
<dd class="col-sm-9">@Model.PhoneNumber</dd>
<dt class="col-sm-3">Tipo</dt>
<dd class="col-sm-9">@Model.UserType</dd>
</dl>

View File

@ -0,0 +1,97 @@
@model WebVentaCoche.Models.CartViewModel
@{
ViewData["Title"] = "Carrito de Compras";
}
<div class="container mt-4">
<h1 class="mb-4">Carrito</h1>
@if (Model.Products == null || !Model.Products.Any())
{
<p>No hay productos en tu carrito.</p>
}
else
{
<p>Productos en el carrito: @Model.Products.Count</p>
<hr />
<table class="table">
<thead>
<tr>
<th>Imagen</th>
<th>Producto</th>
<th>Precio</th>
<th>Cantidad</th>
<th>Subtotal</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var cartItem in Model.Products)
{
<tr>
<td>
@if (!string.IsNullOrEmpty(cartItem.Product.ImagePath))
{
<img src="@cartItem.Product.ImagePath"
alt="@cartItem.Product.Name"
class="img-thumbnail"
style="max-height:100px;" />
}
else
{
<img src="/images/default.png"
alt="Imagen no disponible"
class="img-thumbnail"
style="max-height:100px;" />
}
</td>
<td>@cartItem.Product.Name</td>
<td>@cartItem.Product.Price €</td>
<td>
<form asp-action="UpdateQuantity" asp-controller="Cart" method="post">
<input type="hidden" name="productId" value="@cartItem.Product.Id" />
<input type="number" name="quantity" value="@cartItem.Amount"
min="1" class="form-control d-inline" style="width:70px;" />
<button type="submit" class="btn btn-sm btn-secondary ms-1">
Actualizar
</button>
</form>
</td>
<td>@cartItem.Subtotal €</td>
<td>
<form asp-action="Remove" asp-controller="Cart" method="post">
<input type="hidden" name="id" value="@cartItem.Product.Id" />
<button type="submit" class="btn btn-sm btn-danger">
Eliminar
</button>
</form>
</td>
</tr>
}
</tbody>
</table>
<div class="text-end">
<h4>Total: @Model.Total €</h4>
<div class="mt-3">
<form asp-action="Clear" asp-controller="Cart" method="post" class="d-inline">
<button type="submit" class="btn btn-warning">
Vaciar Carrito
</button>
</form>
<a class="btn btn-primary ms-2" href="#">
Proceder al Pago
</a>
</div>
</div>
}
</div>

View File

@ -4,19 +4,83 @@
<h2 class="text-center my-4">@Model.Name</h2>
<div class="row">
<div class="col-12 mb-4">
<div class="card" style="background-color: rgba(255, 255, 255, 0.8); border-radius: 10px;">
<div class="row g-0">
<div class="col-md-8">
<div class="card-body">
<p>@Model.LongDescription</p>
<div class="d-flex justify-content-between align-items-center">
<p><strong>Precio:</strong> @Model.Price €</p>
<!-- Formulario sin "submit" tradicional -->
<form id="addToCartForm">
<input type="hidden" name="id" value="@Model.Id" />
<button type="submit" class="btn btn-primary">Añadir al carrito</button>
</form>
</div>
<div class="col-md-4">
</div>
</div>
<div class="col-md-4 d-flex align-items-center">
@if (!string.IsNullOrEmpty(Model.ImagePath))
{
<img src="@Model.ImagePath" class="img-fluid rounded" alt="@Model.Name" />
<img src="@Model.ImagePath" class="img-fluid rounded m-3" alt="@Model.Name" />
}
else
{
<img src="/images/default.png" class="img-fluid rounded" alt="Imagen no disponible" />
<img src="/images/default.png" class="img-fluid rounded m-3" alt="Imagen no disponible" />
}
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("addToCartForm");
form.addEventListener("submit", function (event) {
event.preventDefault();
let url = '@Url.Action("AddProductToCart", "Cart")';
let formData = new FormData(form);
fetch(url, {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error("Error en la respuesta del servidor");
}
return response.json();
})
.then(data => {
if (data.success) {
// Mensaje opcional
alert("¡Producto añadido al carrito!");
// 1) Obtener el span (o elemento) que muestra el contador
// Por ejemplo, un <span> con id="cartCountSpan".
const badgeElement = document.getElementById("cartCountSpan");
// 2) Actualizar el contenido del badge
badgeElement.innerText = data.cartCount;
// 3) Mostrarlo si está oculto (si data.cartCount > 0)
if (data.cartCount > 0) {
badgeElement.parentElement.classList.remove("d-none");
// Asumiendo que envuelves el badge en un contenedor con "d-none" cuando es 0
}
}
})
.catch(error => {
console.error(error);
alert("Ocurrió un error al añadir el producto al carrito.");
});
});
});
</script>
}

View File

@ -0,0 +1,39 @@
@using System.Security.Claims
@inject IHttpContextAccessor HttpContextAccessor
@{
Layout = "_Layout";
var userId = HttpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
string currentAction = ViewContext.RouteData.Values["action"]?.ToString() ?? "";
}
<div class="container-fluid mt-4">
<div class="row">
<nav class="col-md-3 col-lg-2 d-none d-md-block bg-light sidebar py-3">
<div class="nav flex-column nav-pills">
<a class="nav-link @(currentAction=="Settings" ? "active" : "")"
asp-controller="Account"
asp-action="Settings">
Cuenta
</a>
<a class="nav-link @(currentAction=="Addresses" ? "active" : "")"
asp-controller="Account"
asp-action="Addresses">
Direcciones
</a>
<a class="nav-link @(currentAction=="Security" ? "active" : "")"
asp-controller="Account"
asp-action="Security">
Seguridad
</a>
</div>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
@RenderBody()
</main>
</div>
</div>
@RenderSection("Scripts", required: false)

View File

@ -0,0 +1,48 @@
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}

View File

@ -1,21 +1,34 @@
@{
@using WebVentaCoche.Helpers
@using WebVentaCoche.Models
@using System.Security.Claims
@inject IHttpContextAccessor HttpContextAccessor
@{
// Determina si es la página de gestión para ajustar la clase body
var isGestionPage = Context.Request.Path.ToString().StartsWith("/Products");
// Obtener lista de IDs de productos almacenados en sesión
var cartItemIds = CartSessionHelper.GetCartItems(HttpContextAccessor.HttpContext.Session);
int cartItemCount = cartItemIds.Count;
var userId = HttpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
<!DOCTYPE html>
<html lang="en">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="~/css/site.css" />
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebVentaCoche</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="~/css/site.css" />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body class="@(isGestionPage ? "no-background" : "")">
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<div class="d-flex justify-content-center col-10">
@ -38,17 +51,31 @@
</li>
</ul>
</div>
<div class="collapse navbar-collapse justify-content-center" id="navbarNav">
<ul class="navbar-nav ms-auto">
@if (User.Identity.IsAuthenticated)
{
<!-- Menú Desplegable con el Icono de Usuario -->
<li class="nav-item me-2 position-relative">
<a class="nav-link" href="@Url.Action("Index", "Cart")">
<i class="fa fa-shopping-cart" style="font-size: 1.5rem;"></i>
@* Si cartItemCount es 0, le metemos clase "d-none" *@
<span id="cartBadge" class="position-absolute top-0 start-100 translate-middle badge
rounded-pill bg-danger @(cartItemCount == 0 ? "d-none" : "")">
<span id="cartCountSpan">@cartItemCount</span>
</span>
</a>
</li>
<!-- Menú desplegable de usuario -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-user" style="font-size: 1.5rem;"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><a class="dropdown-item" href="@Url.Action("Details", "Account")">Detalles Cuenta</a></li>
<li> <a class="dropdown-item" asp-controller="Account" asp-action="Details" asp-route-id="@userId">Detalles Cuenta</a></li>
<li><a class="dropdown-item" asp-controller="Account" asp-action="Settings">Configuración</a></li>
<li><a class="dropdown-item" href="@Url.Action("Index", "Order")">Pedidos</a></li>
<li><hr class="dropdown-divider"></li>
<li>
@ -61,33 +88,33 @@
}
else
{
<!-- Mostrar "Iniciar Sesión" si el usuario no está autenticado -->
<li class="nav-item">
<a class="nav-link" href="@Url.Action("Login", "User")">Iniciar Sesión</a>
</li>
}
</ul>
</div>
</div>
</nav>
<!-- Jumbotron -->
<div class="jumbotron jumbotron-fluid bg-image" style="background-image: url('/images/captura.jpg'); background-size: cover; background-position: center; height: 300px;">
<div class="jumbotron jumbotron-fluid bg-image"
style="background-image: url('/images/captura.jpg');
background-size: cover;
background-position: center;
height: 300px;">
<div class="container text-white text-center">
<h1 class="display-4">Bienvenido a WebVentaCoche</h1>
<p class="lead">Tu tienda de confianza para productos de coches.</p>
</div>
</div>
<!-- Content -->
<div class="container">
@RenderBody()
</div>
<!-- Footer -->
<footer class="bg-dark text-light text-center py-3 mt-4">
<p>&copy; 2024 WebVentaCoche. Todos los derechos reservados.</p>
</footer>
@RenderSection("Scripts", required: false)
</body>
</html>

View File

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />