feat: add WebMessage model and controller with CRUD endpoints #7

This commit is contained in:
Marek Lesko
2025-10-31 15:56:21 +00:00
parent 4c85d18a79
commit 426b4c55fc
6 changed files with 299 additions and 53 deletions

View File

@@ -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<WebMessage> 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<ActionResult<IEnumerable<WebMessage>>> 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<ActionResult<WebMessage>> 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<ActionResult<WebMessage>> 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);
}
}
}

View File

@@ -0,0 +1,87 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
b.Property<decimal>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.HasColumnType("nvarchar(max)");
b.Property<string>("Message")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
b.Property<string>("Phone")
.HasColumnType("nvarchar(max)");
b.Property<string>("Subject")
.HasColumnType("nvarchar(max)");
b.Property<string>("Surname")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("WebMessages");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Api.Migrations
{
/// <inheritdoc />
public partial class AddWebMessages : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "WebMessages",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
Surname = table.Column<string>(type: "nvarchar(max)", nullable: true),
Email = table.Column<string>(type: "nvarchar(max)", nullable: true),
Phone = table.Column<string>(type: "nvarchar(max)", nullable: true),
Subject = table.Column<string>(type: "nvarchar(max)", nullable: true),
Message = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WebMessages", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WebMessages");
}
}
}

View File

@@ -1,53 +1,84 @@
// <auto-generated /> // <auto-generated />
using Api.Models; using Api.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable #nullable disable
namespace Api.Migrations namespace Api.Migrations
{ {
[DbContext(typeof(AppDbContext))] [DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot partial class AppDbContextModelSnapshot : ModelSnapshot
{ {
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.7") .HasAnnotation("ProductVersion", "8.0.18")
.HasAnnotation("Relational:MaxIdentifierLength", 128); .HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Api.Models.Product", b => modelBuilder.Entity("Api.Models.Product", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("int"); .HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<decimal>("Price") b.Property<decimal>("Price")
.HasColumnType("decimal(18,2)"); .HasColumnType("decimal(18,2)");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Products"); b.ToTable("Products");
b.HasData( b.HasData(
new new
{ {
Id = 1, Id = 1,
Name = "Sample Product", Name = "Sample Product",
Price = 9.99m Price = 9.99m
}); });
}); });
#pragma warning restore 612, 618
} modelBuilder.Entity("WebMessage", b =>
} {
} b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.HasColumnType("nvarchar(max)");
b.Property<string>("Message")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
b.Property<string>("Phone")
.HasColumnType("nvarchar(max)");
b.Property<string>("Subject")
.HasColumnType("nvarchar(max)");
b.Property<string>("Surname")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("WebMessages");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -15,6 +15,8 @@ namespace Api.Models
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; } public DbSet<Product> Products { get; set; }
public DbSet<WebMessage> WebMessages { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);

16
Api/Models/WebMessage.cs Normal file
View File

@@ -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; }
}