diff --git a/Seasoned.Backend/Controllers/RecipeController.cs b/Seasoned.Backend/Controllers/RecipeController.cs index 1443428..9753ac9 100644 --- a/Seasoned.Backend/Controllers/RecipeController.cs +++ b/Seasoned.Backend/Controllers/RecipeController.cs @@ -2,7 +2,10 @@ using Microsoft.AspNetCore.Mvc; using Seasoned.Backend.Services; using Seasoned.Backend.DTOs; using Seasoned.Backend.Data; +using System.Security.Claims; using Seasoned.Backend.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; namespace Seasoned.Backend.Controllers; @@ -31,6 +34,7 @@ public class RecipeController : ControllerBase return Ok(result); } + [Authorize] [HttpPost("save")] public async Task SaveRecipe([FromBody] RecipeResponseDto recipeDto) { @@ -39,20 +43,22 @@ public class RecipeController : ControllerBase return BadRequest("Invalid recipe data."); } + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var recipe = new Recipe { Title = recipeDto.Title, Description = recipeDto.Description, Ingredients = recipeDto.Ingredients, Instructions = recipeDto.Instructions, - CreatedAt = DateTime.UtcNow + CreatedAt = DateTime.UtcNow, + UserId = userId }; _context.Recipes.Add(recipe); await _context.SaveChangesAsync(); - return Ok(new { message = "Recipe saved!" }); + return Ok(new { message = "Recipe saved to your collection!" }); } - } \ No newline at end of file diff --git a/Seasoned.Backend/Data/ApplicationDbContext.cs b/Seasoned.Backend/Data/ApplicationDbContext.cs index f90aa29..0fc70ab 100644 --- a/Seasoned.Backend/Data/ApplicationDbContext.cs +++ b/Seasoned.Backend/Data/ApplicationDbContext.cs @@ -1,9 +1,12 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Seasoned.Backend.Models; namespace Seasoned.Backend.Data; -public class ApplicationDbContext : DbContext +// Inherit from IdentityDbContext to enable User management +public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { } @@ -12,6 +15,15 @@ public class ApplicationDbContext : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { + // Crucial: Call the base method so Identity tables are configured + base.OnModelCreating(modelBuilder); + modelBuilder.HasPostgresExtension("vector"); + + // Optional: Ensure the Recipe table links to the Identity User + modelBuilder.Entity() + .HasOne() + .WithMany() + .HasForeignKey(r => r.UserId); } } \ No newline at end of file diff --git a/Seasoned.Backend/Data/Migrations/20260305044721_SeasonedInit.Designer.cs b/Seasoned.Backend/Data/Migrations/20260305044721_SeasonedInit.Designer.cs deleted file mode 100644 index 7e3828e..0000000 --- a/Seasoned.Backend/Data/Migrations/20260305044721_SeasonedInit.Designer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Seasoned.Backend.Data; - -#nullable disable - -namespace Seasoned.Backend.Data.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20260305044721_SeasonedInit")] - partial class SeasonedInit - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Seasoned.Backend.Models.Recipe", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.PrimitiveCollection>("Ingredients") - .IsRequired() - .HasColumnType("text[]"); - - b.PrimitiveCollection>("Instructions") - .IsRequired() - .HasColumnType("text[]"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Recipes"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Seasoned.Backend/Data/Migrations/20260305044721_SeasonedInit.cs b/Seasoned.Backend/Data/Migrations/20260305044721_SeasonedInit.cs deleted file mode 100644 index 9fe118a..0000000 --- a/Seasoned.Backend/Data/Migrations/20260305044721_SeasonedInit.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Seasoned.Backend.Data.Migrations -{ - /// - public partial class SeasonedInit : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:PostgresExtension:vector", ",,"); - - migrationBuilder.CreateTable( - name: "Recipes", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Title = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), - Ingredients = table.Column>(type: "text[]", nullable: false), - Instructions = table.Column>(type: "text[]", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Recipes", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Recipes"); - } - } -} diff --git a/Seasoned.Backend/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Seasoned.Backend/Data/Migrations/ApplicationDbContextModelSnapshot.cs deleted file mode 100644 index 31b2b69..0000000 --- a/Seasoned.Backend/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ /dev/null @@ -1,61 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Seasoned.Backend.Data; - -#nullable disable - -namespace Seasoned.Backend.Data.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - partial class ApplicationDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Seasoned.Backend.Models.Recipe", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.PrimitiveCollection>("Ingredients") - .IsRequired() - .HasColumnType("text[]"); - - b.PrimitiveCollection>("Instructions") - .IsRequired() - .HasColumnType("text[]"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Recipes"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Seasoned.Backend/Migrations/20260306062007_InitialCreate.Designer.cs b/Seasoned.Backend/Migrations/20260306062007_InitialCreate.Designer.cs new file mode 100644 index 0000000..75b8f50 --- /dev/null +++ b/Seasoned.Backend/Migrations/20260306062007_InitialCreate.Designer.cs @@ -0,0 +1,322 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Seasoned.Backend.Data; + +#nullable disable + +namespace Seasoned.Backend.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260306062007_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Seasoned.Backend.Models.Recipe", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.PrimitiveCollection>("Ingredients") + .IsRequired() + .HasColumnType("text[]"); + + b.PrimitiveCollection>("Instructions") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Recipes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Seasoned.Backend.Models.Recipe", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Seasoned.Backend/Migrations/20260306062007_InitialCreate.cs b/Seasoned.Backend/Migrations/20260306062007_InitialCreate.cs new file mode 100644 index 0000000..82439f9 --- /dev/null +++ b/Seasoned.Backend/Migrations/20260306062007_InitialCreate.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Seasoned.Backend.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:vector", ",,"); + + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + RoleId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Recipes", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: true), + Ingredients = table.Column>(type: "text[]", nullable: false), + Instructions = table.Column>(type: "text[]", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UserId = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Recipes", x => x.Id); + table.ForeignKey( + name: "FK_Recipes_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Recipes_UserId", + table: "Recipes", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "Recipes"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/Seasoned.Backend/Migrations/ApplicationDbContextModelSnapshot.cs b/Seasoned.Backend/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..eb0c8d9 --- /dev/null +++ b/Seasoned.Backend/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,319 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Seasoned.Backend.Data; + +#nullable disable + +namespace Seasoned.Backend.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Seasoned.Backend.Models.Recipe", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.PrimitiveCollection>("Ingredients") + .IsRequired() + .HasColumnType("text[]"); + + b.PrimitiveCollection>("Instructions") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Recipes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Seasoned.Backend.Models.Recipe", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Seasoned.Backend/Models/Recipe.cs b/Seasoned.Backend/Models/Recipe.cs index 3192815..38825ad 100644 --- a/Seasoned.Backend/Models/Recipe.cs +++ b/Seasoned.Backend/Models/Recipe.cs @@ -1,11 +1,11 @@ namespace Seasoned.Backend.Models; -public class Recipe -{ +public class Recipe { public int Id { get; set; } public string Title { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; + public string? Description { get; set; } public List Ingredients { get; set; } = new(); public List Instructions { get; set; } = new(); - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime CreatedAt { get; set; } + public string? UserId { get; set; } } \ No newline at end of file diff --git a/Seasoned.Backend/Program.cs b/Seasoned.Backend/Program.cs index cc9c99b..7b88ee6 100644 --- a/Seasoned.Backend/Program.cs +++ b/Seasoned.Backend/Program.cs @@ -3,11 +3,18 @@ using Microsoft.AspNetCore.HttpOverrides; using System.Text.Json; using Microsoft.EntityFrameworkCore; using Seasoned.Backend.Data; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped(); +builder.Services.AddIdentityApiEndpoints() + .AddEntityFrameworkStores(); + +builder.Services.AddAuthorization(); + builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; @@ -17,11 +24,12 @@ builder.Services.AddOpenApi(); builder.Services.AddCors(options => { - options.AddPolicy("AllowAll", policy => + options.AddPolicy("SeasonedOriginPolicy", policy => { - policy.AllowAnyOrigin() + policy.WithOrigins("https://https://seasoned.ddns.net") .AllowAnyMethod() - .AllowAnyHeader(); + .AllowAnyHeader() + .AllowCredentials(); }); }); @@ -39,13 +47,16 @@ using (var scope = app.Services.CreateScope()) app.UseDefaultFiles(); app.UseStaticFiles(); -app.UseCors("AllowAll"); +app.UseCors("SeasonedOriginPolicy"); +app.UseAuthentication(); +app.UseAuthorization(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } +app.MapGroup("/api/auth").MapIdentityApi(); app.MapControllers(); app.MapFallbackToFile("index.html"); app.Run(); \ No newline at end of file diff --git a/Seasoned.Backend/Seasoned.Backend.csproj b/Seasoned.Backend/Seasoned.Backend.csproj index 31c878a..a2bc6df 100644 --- a/Seasoned.Backend/Seasoned.Backend.csproj +++ b/Seasoned.Backend/Seasoned.Backend.csproj @@ -8,6 +8,7 @@ + diff --git a/Seasoned.Frontend/app/assets/css/auth.css b/Seasoned.Frontend/app/assets/css/auth.css new file mode 100644 index 0000000..2f98613 --- /dev/null +++ b/Seasoned.Frontend/app/assets/css/auth.css @@ -0,0 +1,48 @@ +/* app/assets/css/auth.css */ + +.auth-card { + /* Inherits recipe-card but adds a tighter feel for a login form */ + max-width: 450px; + margin-top: 5vh; +} + +.auth-title { + font-family: 'Libre Baskerville', serif; + font-weight: 700; + font-size: 2.2rem; + color: #2e1e0a; + margin-bottom: 8px; +} + +/* Enhancing the custom-input for Auth with icons */ +.auth-input .v-field__prepend-inner { + display: flex !important; /* Overriding the 'display: none' from app-theme.css */ + padding-right: 12px; +} + +.auth-input .v-icon { + color: #f8f1e0 !important; + opacity: 0.6; +} + +.auth-toggle-btn { + font-family: 'Inter', sans-serif !important; + font-size: 0.85rem !important; + color: #6d5e4a !important; + text-decoration: underline; + cursor: pointer; + transition: color 0.2s; +} + +.auth-toggle-btn:hover { + color: #2e1e0a !important; +} + +/* Subtle animation when switching between Login and Register */ +.auth-switch-enter-active, .auth-switch-leave-active { + transition: all 0.3s ease; +} +.auth-switch-enter-from, .auth-switch-leave-to { + opacity: 0; + transform: translateY(10px); +} \ No newline at end of file diff --git a/Seasoned.Frontend/app/pages/auth.vue b/Seasoned.Frontend/app/pages/auth.vue new file mode 100644 index 0000000..a2dad3e --- /dev/null +++ b/Seasoned.Frontend/app/pages/auth.vue @@ -0,0 +1,104 @@ + + + \ No newline at end of file diff --git a/Seasoned.Frontend/nuxt.config.ts b/Seasoned.Frontend/nuxt.config.ts index 9872e54..c93906c 100644 --- a/Seasoned.Frontend/nuxt.config.ts +++ b/Seasoned.Frontend/nuxt.config.ts @@ -13,7 +13,8 @@ export default defineNuxtConfig({ 'vuetify/lib/styles/main.sass', '@mdi/font/css/materialdesignicons.min.css', '~/assets/css/app-theme.css', - '~/assets/css/gallery.css' + '~/assets/css/gallery.css', + '~/assets/css/auth.css' ], build: {