From 426b4c55fccb10dc368498ffd1db827e77953b59 Mon Sep 17 00:00:00 2001 From: Marek Lesko Date: Fri, 31 Oct 2025 15:56:21 +0000 Subject: [PATCH] feat: add WebMessage model and controller with CRUD endpoints #7 --- Api/Controllers/WebMessagesController.cs | 71 +++++++++ .../20251031151451_AddWebMessages.Designer.cs | 87 +++++++++++ .../20251031151451_AddWebMessages.cs | 39 +++++ Api/Migrations/AppDbContextModelSnapshot.cs | 137 +++++++++++------- Api/Models/AppDbContext.cs | 2 + Api/Models/WebMessage.cs | 16 ++ 6 files changed, 299 insertions(+), 53 deletions(-) create mode 100644 Api/Controllers/WebMessagesController.cs create mode 100644 Api/Migrations/20251031151451_AddWebMessages.Designer.cs create mode 100644 Api/Migrations/20251031151451_AddWebMessages.cs create mode 100644 Api/Models/WebMessage.cs diff --git a/Api/Controllers/WebMessagesController.cs b/Api/Controllers/WebMessagesController.cs new file mode 100644 index 0000000..9e76a3b --- /dev/null +++ b/Api/Controllers/WebMessagesController.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Api.Helpers; +using Api.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Api.Controllers +{ + // Controller to expose WebMessage read endpoints for the SPA. + // Assumes an EF DbContext named ApiDbContext with DbSet WebMessages. + [ApiController] + [Route("api/[controller]")] + public class WebMessagesController : ControllerBase + { + private readonly AppDbContext _context; + + public WebMessagesController(AppDbContext context) + { + _context = context; + } + + // GET: api/webmessages + // Optional pagination via ?page=1&pageSize=20 + [HttpGet] + public async Task>> GetAll([FromQuery] int? page, [FromQuery] int? pageSize) + { + var query = _context.WebMessages.AsNoTracking().OrderByDescending(m => m.Id).AsQueryable(); + + if (page.HasValue && pageSize.HasValue && page > 0 && pageSize > 0) + { + var skip = (page.Value - 1) * pageSize.Value; + query = query.Skip(skip).Take(pageSize.Value); + } + + var list = await query.ToListAsync(); + return Ok(list); + } + + // GET: api/webmessages/5 + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var message = await _context.WebMessages.AsNoTracking().FirstOrDefaultAsync(m => m.Id == id); + if (message == null) return NotFound(); + return Ok(message); + } + + // POST: api/webmessages + // Saves a new WebMessage. Expects JSON body. Returns 201 with Location header. + [HttpPost] + public async Task> Create([FromBody] WebMessage message) + { + if (message == null) + return BadRequest(); + + // optional: basic server-side validation + if (string.IsNullOrWhiteSpace(message.Message) && string.IsNullOrWhiteSpace(message.Subject)) + return BadRequest("Message or Subject is required."); + + if(ReCaptchaAssessment.CheckToken(message.RecaptchaToken, out string reason) == false) + return BadRequest($"ReCaptcha validation failed: {reason}"); + + _context.WebMessages.Add(message); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetById), new { id = message.Id }, message); + } + } +} \ No newline at end of file diff --git a/Api/Migrations/20251031151451_AddWebMessages.Designer.cs b/Api/Migrations/20251031151451_AddWebMessages.Designer.cs new file mode 100644 index 0000000..3b5c442 --- /dev/null +++ b/Api/Migrations/20251031151451_AddWebMessages.Designer.cs @@ -0,0 +1,87 @@ +// +using Api.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251031151451_AddWebMessages")] + partial class AddWebMessages + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.18") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Api.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.ToTable("Products"); + + b.HasData( + new + { + Id = 1, + Name = "Sample Product", + Price = 9.99m + }); + }); + + modelBuilder.Entity("WebMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("Subject") + .HasColumnType("nvarchar(max)"); + + b.Property("Surname") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WebMessages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Api/Migrations/20251031151451_AddWebMessages.cs b/Api/Migrations/20251031151451_AddWebMessages.cs new file mode 100644 index 0000000..d4a8ff0 --- /dev/null +++ b/Api/Migrations/20251031151451_AddWebMessages.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class AddWebMessages : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "WebMessages", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: true), + Surname = table.Column(type: "nvarchar(max)", nullable: true), + Email = table.Column(type: "nvarchar(max)", nullable: true), + Phone = table.Column(type: "nvarchar(max)", nullable: true), + Subject = table.Column(type: "nvarchar(max)", nullable: true), + Message = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WebMessages", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "WebMessages"); + } + } +} diff --git a/Api/Migrations/AppDbContextModelSnapshot.cs b/Api/Migrations/AppDbContextModelSnapshot.cs index a363119..f50dbd9 100755 --- a/Api/Migrations/AppDbContextModelSnapshot.cs +++ b/Api/Migrations/AppDbContextModelSnapshot.cs @@ -1,53 +1,84 @@ -// -using Api.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Api.Migrations -{ - [DbContext(typeof(AppDbContext))] - partial class AppDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Api.Models.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("Name") - .HasColumnType("nvarchar(max)"); - - b.Property("Price") - .HasColumnType("decimal(18,2)"); - - b.HasKey("Id"); - - b.ToTable("Products"); - - b.HasData( - new - { - Id = 1, - Name = "Sample Product", - Price = 9.99m - }); - }); -#pragma warning restore 612, 618 - } - } -} +// +using Api.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.18") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Api.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.ToTable("Products"); + + b.HasData( + new + { + Id = 1, + Name = "Sample Product", + Price = 9.99m + }); + }); + + modelBuilder.Entity("WebMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("Subject") + .HasColumnType("nvarchar(max)"); + + b.Property("Surname") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WebMessages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Api/Models/AppDbContext.cs b/Api/Models/AppDbContext.cs index 239ba6a..5660535 100755 --- a/Api/Models/AppDbContext.cs +++ b/Api/Models/AppDbContext.cs @@ -15,6 +15,8 @@ namespace Api.Models public AppDbContext(DbContextOptions options) : base(options) { } public DbSet Products { get; set; } + public DbSet WebMessages { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); diff --git a/Api/Models/WebMessage.cs b/Api/Models/WebMessage.cs new file mode 100644 index 0000000..e2c0bb3 --- /dev/null +++ b/Api/Models/WebMessage.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +public class WebMessage +{ + [Key] + public int Id { get; set; } + public string? Name { get; set; } + public string? Surname { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public string? Subject { get; set; } + public string? Message { get; set; } + [NotMapped] + public string? RecaptchaToken { get; set; } + } \ No newline at end of file