From c144009172072d67538b8eb6ceb02e63cdbff1b2 Mon Sep 17 00:00:00 2001 From: Snorre Aldstedt Date: Fri, 22 Aug 2025 11:07:00 +0200 Subject: [PATCH 1/5] Create models classes --- .../api-cinema-challenge/DTOs/CustomerGet.cs | 6 ++++ .../api-cinema-challenge/DTOs/CustomerPut.cs | 6 ++++ .../api-cinema-challenge/DTOs/MovieGet.cs | 6 ++++ .../api-cinema-challenge/DTOs/MoviePut.cs | 6 ++++ .../api-cinema-challenge/DTOs/ScreeningGet.cs | 6 ++++ .../api-cinema-challenge/DTOs/ScreeningPut.cs | 6 ++++ .../Endpoints/CustomerEndpoint.cs | 6 ++++ .../Endpoints/MovieEndpoint.cs | 6 ++++ .../Endpoints/ScreeningEndpoint.cs | 6 ++++ .../api-cinema-challenge/Models/Customer.cs | 31 +++++++++++++++++++ .../api-cinema-challenge/Models/Movie.cs | 30 ++++++++++++++++++ .../api-cinema-challenge/Models/Screening.cs | 28 +++++++++++++++++ .../Repository/IRepository.cs | 6 ++++ .../Repository/Repository.cs | 6 ++++ 14 files changed, 155 insertions(+) create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/MovieGet.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPut.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Customer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Movie.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Screening.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs new file mode 100644 index 00000000..cda7409e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.DTOs +{ + public class CustomerGet + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs new file mode 100644 index 00000000..497d97aa --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.DTOs +{ + public class CustomerPut + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGet.cs new file mode 100644 index 00000000..22c54c67 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGet.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.DTOs +{ + public class MovieGet + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs new file mode 100644 index 00000000..f364ee0e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.DTOs +{ + public class MoviePut + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs new file mode 100644 index 00000000..f6e67dce --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.DTOs +{ + public class ScreeningGet + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPut.cs new file mode 100644 index 00000000..7b7ae16c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPut.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.DTOs +{ + public class ScreeningPut + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs new file mode 100644 index 00000000..df19497e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Endpoints +{ + public class CustomerEndpoint + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs new file mode 100644 index 00000000..897b8e5c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Endpoints +{ + public class MovieEndpoint + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs new file mode 100644 index 00000000..4c4f95d9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Endpoints +{ + public class ScreeningEndpoint + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs new file mode 100644 index 00000000..e8c33625 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Net.Mail; + + +namespace api_cinema_challenge.Models +{ + [Table("Customers")] + public class Customer + { + [Key] + public int Id { get; set; } + + [Column("name")] + public string Name { get; set; } + + [Column("email")] + public string Email { get; set; } + + [Column("phone")] + public string Phone { get; set; } + + [Column("createdAt")] + public DateTime CreatedAt { get; set; } + + [Column("updatedAt")] + public DateTime UpdatedAt { get; set; } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs new file mode 100644 index 00000000..b75876b8 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("Movies")] + public class Movie + { + [Key] + public int Id { get; set; } + + [Column("title")] + public string Title { get; set; } + + [Column("rating")] + public string Rating { get; set; } + + [Column("description")] + public string Description { get; set; } + + [Column("runtimeMins")] + public int RuntimeMins { get; set; } + + [Column("createdAt")] + public DateTime CreatedAt { get; set; } + + [Column("updatedAt")] + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs new file mode 100644 index 00000000..b4aaf0b5 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("Screenings")] + public class Screening + { + [Key] + public int Id { get; set; } + + [Column("screenNumber")] + public int ScreenNumber { get; set; } + + [Column("capacity")] + public int Capacity { get; set; } + + [Column("startsAt")] + public DateTime StartsAt { get; set; } + + [Column("createdAt")] + public DateTime CreatedAt { get; set; } + + [Column("updatedAt")] + public DateTime UpdatedAt { get; set; } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs new file mode 100644 index 00000000..f890e988 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Repository +{ + public interface IRepository + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs new file mode 100644 index 00000000..0428cb98 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Repository +{ + public class Repository + { + } +} From e1321493adf8c54215dd96dcb2c7093cd897dddb Mon Sep 17 00:00:00 2001 From: Snorre Aldstedt Date: Fri, 22 Aug 2025 12:55:36 +0200 Subject: [PATCH 2/5] Update classes to migrate to database --- .../api-cinema-challenge/DTOs/CustomerPut.cs | 3 + .../Data/CinemaContext.cs | 21 +- .../20250822105242_First.Designer.cs | 216 ++++++++++++++++++ .../Migrations/20250822105242_First.cs | 133 +++++++++++ .../Migrations/CinemaContextModelSnapshot.cs | 213 +++++++++++++++++ .../api-cinema-challenge/Models/Customer.cs | 2 + .../api-cinema-challenge/Models/Movie.cs | 3 + .../api-cinema-challenge/Models/Screening.cs | 7 + .../api-cinema-challenge/Models/Ticket.cs | 26 +++ .../api-cinema-challenge.csproj | 8 +- 10 files changed, 627 insertions(+), 5 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs index 497d97aa..f02ae3ee 100644 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs @@ -2,5 +2,8 @@ { public class CustomerPut { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index ad4fe854..f93b7577 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; namespace api_cinema_challenge.Data @@ -20,6 +21,24 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { + //modelBuilder.Entity().HasMany(t => t.Tickets); + /*modelBuilder.Entity() + .HasMany(c => c.Tickets) + .WithOne(c => c.Customer);*/ + modelBuilder.Entity() + .HasOne(t => t.Screening) + .WithMany(t => t.Tickets) + .HasForeignKey(t => t.ScreeningId); + + modelBuilder.Entity() + .HasOne(t => t.Customer) + .WithMany(t => t.Tickets) + .HasForeignKey(t => t.CustomerId); + + modelBuilder.Entity() + .HasOne(s => s.Movie) + .WithMany(s => s.Screenings) + .HasForeignKey(s => s.MovieId); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.Designer.cs new file mode 100644 index 00000000..3556a105 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.Designer.cs @@ -0,0 +1,216 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + [Migration("20250822105242_First")] + partial class First + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("runtimeMins"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screenNumber"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("startsAt"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer") + .HasColumnName("numSeats"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.HasOne("api_cinema_challenge.Models.Movie", "Movie") + .WithMany("Screenings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.HasOne("api_cinema_challenge.Models.Customer", "Customer") + .WithMany("Tickets") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany("Tickets") + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.cs new file mode 100644 index 00000000..8dc24197 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.cs @@ -0,0 +1,133 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class First : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "text", nullable: false), + email = table.Column(type: "text", nullable: false), + phone = table.Column(type: "text", nullable: false), + createdAt = table.Column(type: "timestamp with time zone", nullable: false), + updatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Movies", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + title = table.Column(type: "text", nullable: false), + rating = table.Column(type: "text", nullable: false), + description = table.Column(type: "text", nullable: false), + runtimeMins = table.Column(type: "integer", nullable: false), + createdAt = table.Column(type: "timestamp with time zone", nullable: false), + updatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Movies", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Screenings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + screenNumber = table.Column(type: "integer", nullable: false), + capacity = table.Column(type: "integer", nullable: false), + startsAt = table.Column(type: "timestamp with time zone", nullable: false), + createdAt = table.Column(type: "timestamp with time zone", nullable: false), + updatedAt = table.Column(type: "timestamp with time zone", nullable: false), + MovieId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Screenings", x => x.Id); + table.ForeignKey( + name: "FK_Screenings_Movies_MovieId", + column: x => x.MovieId, + principalTable: "Movies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + numSeats = table.Column(type: "integer", nullable: false), + CustomerId = table.Column(type: "integer", nullable: false), + ScreeningId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => x.Id); + table.ForeignKey( + name: "FK_Tickets_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Tickets_Screenings_ScreeningId", + column: x => x.ScreeningId, + principalTable: "Screenings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Screenings_MovieId", + table: "Screenings", + column: "MovieId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_CustomerId", + table: "Tickets", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_ScreeningId", + table: "Tickets", + column: "ScreeningId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Tickets"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Screenings"); + + migrationBuilder.DropTable( + name: "Movies"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs new file mode 100644 index 00000000..cc40824d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,213 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + partial class CinemaContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("runtimeMins"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screenNumber"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("startsAt"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer") + .HasColumnName("numSeats"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.HasOne("api_cinema_challenge.Models.Movie", "Movie") + .WithMany("Screenings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.HasOne("api_cinema_challenge.Models.Customer", "Customer") + .WithMany("Tickets") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany("Tickets") + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs index e8c33625..8b14f2bc 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -27,5 +27,7 @@ public class Customer [Column("updatedAt")] public DateTime UpdatedAt { get; set; } + public List Tickets { get; set; }= new List(); + } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs index b75876b8..212495c2 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -26,5 +26,8 @@ public class Movie [Column("updatedAt")] public DateTime UpdatedAt { get; set; } + + public List Screenings { get; set; } = new List(); + } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs index b4aaf0b5..958a4c1c 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -24,5 +24,12 @@ public class Screening [Column("updatedAt")] public DateTime UpdatedAt { get; set; } + [ForeignKey("Movies")] + public int MovieId { get; set; } + + public Movie Movie { get; set; } + + public List Tickets { get; set; } = new List(); + } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs new file mode 100644 index 00000000..3f019470 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("Tickets")] + public class Ticket + { + [Key] + public int Id { get; set; } + + [Column("numSeats")] + public int NumSeats { get; set; } + + [ForeignKey("Customers")] + public int CustomerId { get; set; } + + public Customer Customer { get; set; } + + [ForeignKey("Screening")] + public int ScreeningId { get; set; } + + public Screening Screening { get; set; } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj index 11e5c66b..f56be7d5 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -17,6 +17,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -27,8 +31,4 @@ - - - - From 042257fe28db16e31307e0f28800c561e1239235 Mon Sep 17 00:00:00 2001 From: Snorre Aldstedt Date: Mon, 25 Aug 2025 09:54:30 +0200 Subject: [PATCH 3/5] Complete core --- .../api-cinema-challenge/DTOs/CustomerGet.cs | 6 ++ .../DTOs/{CustomerPut.cs => CustomerPost.cs} | 2 +- .../api-cinema-challenge/DTOs/MovieGet.cs | 9 ++ .../api-cinema-challenge/DTOs/MoviePost.cs | 10 ++ .../api-cinema-challenge/DTOs/MoviePut.cs | 6 -- .../api-cinema-challenge/DTOs/ScreeningGet.cs | 8 ++ .../DTOs/ScreeningPost.cs | 10 ++ .../api-cinema-challenge/DTOs/ScreeningPut.cs | 6 -- .../Data/CinemaContext.cs | 1 + .../Endpoints/CustomerEndpoint.cs | 86 ++++++++++++++++- .../Endpoints/MovieEndpoint.cs | 94 ++++++++++++++++++- .../Endpoints/ScreeningEndpoint.cs | 73 +++++++++++++- ....cs => 20250822121753_Working.Designer.cs} | 22 ++--- ...242_First.cs => 20250822121753_Working.cs} | 20 ++-- .../Migrations/CinemaContextModelSnapshot.cs | 18 ++-- .../api-cinema-challenge/Models/Customer.cs | 7 +- .../api-cinema-challenge/Models/Movie.cs | 6 +- .../api-cinema-challenge/Models/Screening.cs | 6 +- .../api-cinema-challenge/Models/Ticket.cs | 2 +- .../api-cinema-challenge/Program.cs | 27 +++++- .../Repository/IRepository.cs | 14 ++- .../Repository/Repository.cs | 58 +++++++++++- 22 files changed, 427 insertions(+), 64 deletions(-) rename api-cinema-challenge/api-cinema-challenge/DTOs/{CustomerPut.cs => CustomerPost.cs} (85%) create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs delete mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs delete mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPut.cs rename api-cinema-challenge/api-cinema-challenge/Migrations/{20250822105242_First.Designer.cs => 20250822121753_Working.Designer.cs} (92%) rename api-cinema-challenge/api-cinema-challenge/Migrations/{20250822105242_First.cs => 20250822121753_Working.cs} (89%) diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs index cda7409e..9857c448 100644 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs @@ -2,5 +2,11 @@ { public class CustomerGet { + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPost.cs similarity index 85% rename from api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs rename to api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPost.cs index f02ae3ee..7b36bb48 100644 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPost.cs @@ -1,6 +1,6 @@ namespace api_cinema_challenge.DTOs { - public class CustomerPut + public class CustomerPost { public string Name { get; set; } public string Email { get; set; } diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGet.cs index 22c54c67..96c89222 100644 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGet.cs +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGet.cs @@ -2,5 +2,14 @@ { public class MovieGet { + public int Id { get; set; } + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RunTimeMins { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } + } diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs new file mode 100644 index 00000000..759d4659 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs +{ + public class MoviePost + { + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RunTimeMins { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs deleted file mode 100644 index f364ee0e..00000000 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace api_cinema_challenge.DTOs -{ - public class MoviePut - { - } -} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs index f6e67dce..74da8cd6 100644 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs @@ -2,5 +2,13 @@ { public class ScreeningGet { + public int MovieId { get; set; } + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + + } } diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs new file mode 100644 index 00000000..b02a2b97 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs +{ + public class ScreeningPost + { + //public int MovieId { get; set; } + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPut.cs deleted file mode 100644 index 7b7ae16c..00000000 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPut.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace api_cinema_challenge.DTOs -{ - public class ScreeningPut - { - } -} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index f93b7577..2061b189 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,6 +1,7 @@ using api_cinema_challenge.Models; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; +using Microsoft.AspNetCore.Mvc; namespace api_cinema_challenge.Data { diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs index df19497e..3df19849 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -1,6 +1,88 @@ -namespace api_cinema_challenge.Endpoints +//using Microsoft.AspNetCore.Mvc; +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Endpoints { - public class CustomerEndpoint + public static class CustomerEndpoint { + public static void ConfigureCustomerEndpoints(this WebApplication app) + { + var customer = app.MapGroup("customers"); + + customer.MapGet("/", GetAll); + customer.MapPost("/", Create); + customer.MapPut("/{id}", Update); + customer.MapDelete("/{id}", Delete); + + } + + private static CustomerGet CustomerToCustomerGet(Customer c) + { + CustomerGet customerShow = new CustomerGet() {Id = c.Id, Name = c.Name, Email = c.Email, Phone = c.Phone, CreatedAt = c.CreatedAt, UpdatedAt = c.UpdatedAt }; + return customerShow; + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository customerRepo) { + List response = new List(); + var results = await customerRepo.GetAll(); + foreach (Customer c in results) { + CustomerGet customerShow = CustomerToCustomerGet(c); + response.Add(customerShow); + } + return TypedResults.Ok(response); + } + + + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task Create(IRepository customerRepo, CustomerPost cModel) + { + DateTime time = DateTime.UtcNow.ToUniversalTime(); + Customer newCustomer = new Customer() { + Name = cModel.Name, + Email = cModel.Email, + Phone = cModel.Phone, + CreatedAt = time, + UpdatedAt = time + }; + await customerRepo.Insert(newCustomer); + return TypedResults.Created($"Created object with id: {newCustomer.Id}"); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public static async Task Update(IRepository customerRepo, int id, CustomerPost cModel) + { + Customer? cTarget = await customerRepo.GetById(id); + if(cTarget!= null) + { + DateTime UpdatedTime = DateTime.UtcNow.ToUniversalTime(); + cTarget.Name = cModel.Name; + cTarget.Email = cModel.Email; + cTarget.Phone = cModel.Phone; + cTarget.UpdatedAt = UpdatedTime; + + await customerRepo.Update(cTarget); + return TypedResults.Created($"Updated object with id: {id}"); + } + return TypedResults.NotFound(); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public static async Task Delete(IRepository customerRepo, int id) + { + Customer? cTarget = await customerRepo.GetById(id); + if (cTarget != null) + { + await customerRepo.Delete(id); + return TypedResults.Ok(CustomerToCustomerGet(cTarget)); + } + return TypedResults.NotFound(); + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs index 897b8e5c..d6d2e8da 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -1,6 +1,96 @@ -namespace api_cinema_challenge.Endpoints +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Mvc; +using System.Runtime.InteropServices; + +namespace api_cinema_challenge.Endpoints { - public class MovieEndpoint + public static class MovieEndpoint { + public static void ConfigureMovieEndpoints(this WebApplication app) + { + var movie = app.MapGroup("movies"); + + movie.MapGet("/", GetAll); + movie.MapPost("/", Create); + movie.MapPut("/{id}", Update); + movie.MapDelete("/{id}", Delete); + + } + + public static MovieGet MovieToMovieGet(Movie m) + { + MovieGet movieShow = new MovieGet() { Id = m.Id, Title = m.Title, Rating = m.Rating, Description = m.Description, RunTimeMins = m.RuntimeMins, CreatedAt = m.CreatedAt, UpdatedAt = m.UpdatedAt }; + return movieShow; + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository movieRepo) + { + List response = new List(); + var results = await movieRepo.GetAll(); + foreach (Movie m in results) + { + MovieGet movieShow = MovieToMovieGet(m); + response.Add(movieShow); + } + return TypedResults.Ok(response); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task Create(IRepository movieRepo, MoviePost mModel) + { + DateTime time = DateTime.UtcNow.ToUniversalTime(); + Movie newMovie = new Movie() + { + Title = mModel.Title, + Rating = mModel.Rating, + Description = mModel.Description, + RuntimeMins = mModel.RunTimeMins, + CreatedAt = time, + UpdatedAt = time + }; + await movieRepo.Insert(newMovie); + return TypedResults.Created($"Created object with id: {newMovie.Id}"); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public static async Task Update(IRepository movieRepo, int id, MoviePost mModel) + { + Movie? mTarget = await movieRepo.GetById(id); + + if (mTarget != null) + { + DateTime time = DateTime.UtcNow.ToUniversalTime(); + mTarget.Title = mModel.Title; + mTarget.Rating = mModel.Rating; + mTarget.Description = mModel.Description; + mTarget.RuntimeMins = mModel.RunTimeMins; + mTarget.UpdatedAt = time; + + + + await movieRepo.Update(mTarget); + return TypedResults.Created($"Updated object with id: {id}"); + } + return TypedResults.NotFound(); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public static async Task Delete(IRepository movieRepo, int id) + { + Movie? mTarget = await movieRepo.GetById(id); + if (mTarget != null) + { + await movieRepo.Delete(id); + return TypedResults.Ok(MovieToMovieGet(mTarget)); + } + return TypedResults.NotFound(); + } + } } + diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs index 4c4f95d9..ab13e106 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs @@ -1,6 +1,75 @@ -namespace api_cinema_challenge.Endpoints +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints { - public class ScreeningEndpoint + public static class ScreeningEndpoint { + public static void ConfigureScreeningEndpoints(this WebApplication app) + { + var screening = app.MapGroup("movies/"); + + screening.MapGet("{id}/screenings", GetAll); + screening.MapPost("{id}/screenings", Create); + } + + public static ScreeningGet ScreenToScreeningGet(Screening s) + { + ScreeningGet screenShow = new ScreeningGet() { MovieId = s.MovieId, ScreenNumber = s.ScreenNumber, Capacity = s.Capacity, StartsAt = s.StartsAt, CreatedAt = s.CreatedAt, UpdatedAt = s.UpdatedAt }; + return screenShow; + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository screenRepo, int id) + { + List response = new List(); + var results = await screenRepo.GetAll(); + foreach (Screening s in results) + { + if (s.MovieId == id) + { + ScreeningGet screenShow = ScreenToScreeningGet(s); + response.Add(screenShow); + } + } + return TypedResults.Ok(response); + } + + /* + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository screenRepo, int id) + { + List response = new List(); + var movie = await screenRepo.GetWithIncludes(m => m.Screenings); + var results = await screenRepo.GetAll(); + foreach (Screening s in results) + { + if (s.MovieId == id) + { + ScreeningGet screenShow = ScreenToScreeningGet(s); + response.Add(screenShow); + } + } + return TypedResults.Ok(response); + } + */ + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task Create(IRepository screenRepo, ScreeningPost sModel, int id) + { + DateTime time = DateTime.UtcNow.ToUniversalTime(); + Screening newScreen = new Screening() + { + MovieId = id, + ScreenNumber = sModel.ScreenNumber, + Capacity = sModel.Capacity, + StartsAt = sModel.StartsAt, + CreatedAt = time, + UpdatedAt = time + }; + await screenRepo.Insert(newScreen); + return TypedResults.Created($"Created object with id: {newScreen.Id}"); + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.Designer.cs similarity index 92% rename from api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.Designer.cs rename to api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.Designer.cs index 3556a105..96c7e0d8 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.Designer.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.Designer.cs @@ -12,8 +12,8 @@ namespace api_cinema_challenge.Migrations { [DbContext(typeof(CinemaContext))] - [Migration("20250822105242_First")] - partial class First + [Migration("20250822121753_Working")] + partial class Working { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -35,7 +35,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("createdAt"); + .HasColumnName("createdat"); b.Property("Email") .IsRequired() @@ -54,7 +54,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("updatedAt"); + .HasColumnName("updatedat"); b.HasKey("Id"); @@ -71,7 +71,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("createdAt"); + .HasColumnName("createdat"); b.Property("Description") .IsRequired() @@ -85,7 +85,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("RuntimeMins") .HasColumnType("integer") - .HasColumnName("runtimeMins"); + .HasColumnName("runtimemins"); b.Property("Title") .IsRequired() @@ -94,7 +94,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("updatedAt"); + .HasColumnName("updatedat"); b.HasKey("Id"); @@ -115,7 +115,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("createdAt"); + .HasColumnName("createdat"); b.Property("MovieId") .HasColumnType("integer"); @@ -126,11 +126,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("StartsAt") .HasColumnType("timestamp with time zone") - .HasColumnName("startsAt"); + .HasColumnName("startsat"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("updatedAt"); + .HasColumnName("updatedat"); b.HasKey("Id"); @@ -152,7 +152,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("NumSeats") .HasColumnType("integer") - .HasColumnName("numSeats"); + .HasColumnName("numseats"); b.Property("ScreeningId") .HasColumnType("integer"); diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.cs similarity index 89% rename from api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.cs rename to api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.cs index 8dc24197..48eae7bc 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822105242_First.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.cs @@ -7,7 +7,7 @@ namespace api_cinema_challenge.Migrations { /// - public partial class First : Migration + public partial class Working : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -21,8 +21,8 @@ protected override void Up(MigrationBuilder migrationBuilder) name = table.Column(type: "text", nullable: false), email = table.Column(type: "text", nullable: false), phone = table.Column(type: "text", nullable: false), - createdAt = table.Column(type: "timestamp with time zone", nullable: false), - updatedAt = table.Column(type: "timestamp with time zone", nullable: false) + createdat = table.Column(type: "timestamp with time zone", nullable: false), + updatedat = table.Column(type: "timestamp with time zone", nullable: false) }, constraints: table => { @@ -38,9 +38,9 @@ protected override void Up(MigrationBuilder migrationBuilder) title = table.Column(type: "text", nullable: false), rating = table.Column(type: "text", nullable: false), description = table.Column(type: "text", nullable: false), - runtimeMins = table.Column(type: "integer", nullable: false), - createdAt = table.Column(type: "timestamp with time zone", nullable: false), - updatedAt = table.Column(type: "timestamp with time zone", nullable: false) + runtimemins = table.Column(type: "integer", nullable: false), + createdat = table.Column(type: "timestamp with time zone", nullable: false), + updatedat = table.Column(type: "timestamp with time zone", nullable: false) }, constraints: table => { @@ -55,9 +55,9 @@ protected override void Up(MigrationBuilder migrationBuilder) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), screenNumber = table.Column(type: "integer", nullable: false), capacity = table.Column(type: "integer", nullable: false), - startsAt = table.Column(type: "timestamp with time zone", nullable: false), - createdAt = table.Column(type: "timestamp with time zone", nullable: false), - updatedAt = table.Column(type: "timestamp with time zone", nullable: false), + startsat = table.Column(type: "timestamp with time zone", nullable: false), + createdat = table.Column(type: "timestamp with time zone", nullable: false), + updatedat = table.Column(type: "timestamp with time zone", nullable: false), MovieId = table.Column(type: "integer", nullable: false) }, constraints: table => @@ -77,7 +77,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - numSeats = table.Column(type: "integer", nullable: false), + numseats = table.Column(type: "integer", nullable: false), CustomerId = table.Column(type: "integer", nullable: false), ScreeningId = table.Column(type: "integer", nullable: false) }, diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs index cc40824d..1f2539dd 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -32,7 +32,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("createdAt"); + .HasColumnName("createdat"); b.Property("Email") .IsRequired() @@ -51,7 +51,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("updatedAt"); + .HasColumnName("updatedat"); b.HasKey("Id"); @@ -68,7 +68,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("createdAt"); + .HasColumnName("createdat"); b.Property("Description") .IsRequired() @@ -82,7 +82,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RuntimeMins") .HasColumnType("integer") - .HasColumnName("runtimeMins"); + .HasColumnName("runtimemins"); b.Property("Title") .IsRequired() @@ -91,7 +91,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("updatedAt"); + .HasColumnName("updatedat"); b.HasKey("Id"); @@ -112,7 +112,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("createdAt"); + .HasColumnName("createdat"); b.Property("MovieId") .HasColumnType("integer"); @@ -123,11 +123,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("StartsAt") .HasColumnType("timestamp with time zone") - .HasColumnName("startsAt"); + .HasColumnName("startsat"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") - .HasColumnName("updatedAt"); + .HasColumnName("updatedat"); b.HasKey("Id"); @@ -149,7 +149,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("NumSeats") .HasColumnType("integer") - .HasColumnName("numSeats"); + .HasColumnName("numseats"); b.Property("ScreeningId") .HasColumnType("integer"); diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs index 8b14f2bc..5212a2f3 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Net.Mail; +using Microsoft.AspNetCore.Mvc; namespace api_cinema_challenge.Models @@ -21,13 +22,13 @@ public class Customer [Column("phone")] public string Phone { get; set; } - [Column("createdAt")] + [Column("createdat")] public DateTime CreatedAt { get; set; } - [Column("updatedAt")] + [Column("updatedat")] public DateTime UpdatedAt { get; set; } - public List Tickets { get; set; }= new List(); + public IEnumerable Tickets { get; set; }= new List(); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs index 212495c2..d6ebf939 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -18,13 +18,13 @@ public class Movie [Column("description")] public string Description { get; set; } - [Column("runtimeMins")] + [Column("runtimemins")] public int RuntimeMins { get; set; } - [Column("createdAt")] + [Column("createdat")] public DateTime CreatedAt { get; set; } - [Column("updatedAt")] + [Column("updatedat")] public DateTime UpdatedAt { get; set; } public List Screenings { get; set; } = new List(); diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs index 958a4c1c..c648a929 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -15,13 +15,13 @@ public class Screening [Column("capacity")] public int Capacity { get; set; } - [Column("startsAt")] + [Column("startsat")] public DateTime StartsAt { get; set; } - [Column("createdAt")] + [Column("createdat")] public DateTime CreatedAt { get; set; } - [Column("updatedAt")] + [Column("updatedat")] public DateTime UpdatedAt { get; set; } [ForeignKey("Movies")] diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs index 3f019470..e78f3dfb 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -9,7 +9,7 @@ public class Ticket [Key] public int Id { get; set; } - [Column("numSeats")] + [Column("numseats")] public int NumSeats { get; set; } [ForeignKey("Customers")] diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..23d6327d 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,20 +1,45 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.EntityFrameworkCore; +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.AddOpenApi(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); + + +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); + options.LogTo(message => Debug.WriteLine(message)); +}); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { + app.MapOpenApi(); app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwaggerUI( options => + { + options.SwaggerEndpoint("/openapi/v1.json", "Cinema API"); + }); } app.UseHttpsRedirection(); + +app.ConfigureCustomerEndpoints(); +app.ConfigureMovieEndpoints(); +app.ConfigureScreeningEndpoints(); app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs index f890e988..45af234f 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs @@ -1,6 +1,16 @@ -namespace api_cinema_challenge.Repository +using System.Linq.Expressions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository { - public interface IRepository + public interface IRepository { + Task> GetAll(); + Task GetById(int id); + Task Update(T entity); + Task Delete(int id); + Task Insert(T entity); + Task> GetWithIncludes(Func, IQueryable> includeQuery); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs index 0428cb98..821186a8 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs @@ -1,6 +1,60 @@ -namespace api_cinema_challenge.Repository +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace api_cinema_challenge.Repository { - public class Repository + public class Repository : IRepository where T : class { + private CinemaContext _database; + private DbSet _table = null!; + + public Repository(CinemaContext cinemaContext) + { + _database = cinemaContext; + _table = _database.Set(); + } + + public async Task Insert(T entity) + { + await _table.AddAsync(entity); + await _database.SaveChangesAsync(); + return entity; + } + + public async Task Delete(int id) + { + T entity = await _table.FindAsync(id); + _table.Remove(entity); + await _database.SaveChangesAsync(); + return entity; + } + + public async Task> GetAll() + { + return await _table.ToListAsync(); + } + + public async Task GetById(int id) + { + T entity = await _table.FindAsync(id); + return entity; + } + + public async Task Update(T entity) + { + _table.Attach(entity); + _database.Entry(entity).State = EntityState.Modified; + await _database.SaveChangesAsync(); + return entity; + } + + public async Task> GetWithIncludes(Func, IQueryable> includeQuery) + { + IQueryable query = includeQuery(_table); + return await query.ToListAsync(); + } + + } } From ade7cb756399717bfead8d093168f28bb74c36d0 Mon Sep 17 00:00:00 2001 From: Snorre Aldstedt Date: Mon, 25 Aug 2025 15:23:53 +0200 Subject: [PATCH 4/5] Update ticket --- .../api-cinema-challenge/DTOs/TicketGet.cs | 12 +++++ .../api-cinema-challenge/DTOs/TicketPost.cs | 7 +++ .../Endpoints/ScreeningEndpoint.cs | 10 ++-- .../Endpoints/TicketEndpoint.cs | 53 +++++++++++++++++++ ...> 20250825130218_UpdateTicket.Designer.cs} | 12 ++++- ...king.cs => 20250825130218_UpdateTicket.cs} | 6 ++- .../Migrations/CinemaContextModelSnapshot.cs | 8 +++ .../api-cinema-challenge/Models/Ticket.cs | 8 ++- .../api-cinema-challenge/Program.cs | 2 + 9 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/TicketGet.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/TicketPost.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs rename api-cinema-challenge/api-cinema-challenge/Migrations/{20250822121753_Working.Designer.cs => 20250825130218_UpdateTicket.Designer.cs} (94%) rename api-cinema-challenge/api-cinema-challenge/Migrations/{20250822121753_Working.cs => 20250825130218_UpdateTicket.cs} (95%) diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/TicketGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketGet.cs new file mode 100644 index 00000000..ec1fed89 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketGet.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.DTOs +{ + public class TicketGet + { + public int Id { get; set; } + + public int NumSeats { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/TicketPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketPost.cs new file mode 100644 index 00000000..b05eccb3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketPost.cs @@ -0,0 +1,7 @@ +namespace api_cinema_challenge.DTOs +{ + public class TicketPost + { + public int NumSeats { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs index ab13e106..6fe3d4d1 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs @@ -25,14 +25,12 @@ public static ScreeningGet ScreenToScreeningGet(Screening s) public static async Task GetAll(IRepository screenRepo, int id) { List response = new List(); - var results = await screenRepo.GetAll(); + var all_results = await screenRepo.GetAll(); + var results = all_results.Where(s => s.MovieId == id); foreach (Screening s in results) { - if (s.MovieId == id) - { - ScreeningGet screenShow = ScreenToScreeningGet(s); - response.Add(screenShow); - } + ScreeningGet screenShow = ScreenToScreeningGet(s); + response.Add(screenShow); } return TypedResults.Ok(response); } diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs new file mode 100644 index 00000000..35609c25 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs @@ -0,0 +1,53 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace api_cinema_challenge.Endpoints +{ + public static class TicketEndpoint + { + public static void ConfigureTicketEndpoints(this WebApplication app) + { + var ticket = app.MapGroup("customers/"); + + ticket.MapGet("{cId}/screenings{sId}", GetAll); + ticket.MapPost("{cId}/screenings{sId}", Create); + } + + private static TicketGet TicketToTicketGet(Ticket t) + { + TicketGet ticketGet = new TicketGet() { Id = t.Id, NumSeats = t.NumSeats, CreatedAt = t.CreatedAt, UpdatedAt = t.UpdatedAt }; + return ticketGet; + } + + public static async Task GetAll(IRepository ticketRepo, int cId, int sId) + { + //List results = new List(); + var tickets = await ticketRepo.GetAll(); + var results = tickets.Where(t => t.CustomerId == cId && t.ScreeningId == sId).ToList(); + var resultsShow = new List(); + foreach (var result in results) + { + resultsShow.Add(TicketToTicketGet(result)); + } + return TypedResults.Ok(resultsShow); + } + + public static async Task Create(IRepository ticketRepo, int cId, int sId, TicketPost t) + { + DateTime time = DateTime.UtcNow.ToUniversalTime(); + + Ticket ticket = new Ticket() + { + NumSeats = t.NumSeats, + CustomerId = cId, + ScreeningId = sId, + CreatedAt = time, + UpdatedAt = time + }; + await ticketRepo.Insert(ticket); + return TypedResults.Ok(TicketToTicketGet(ticket)); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.Designer.cs similarity index 94% rename from api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.Designer.cs rename to api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.Designer.cs index 96c7e0d8..215d9a59 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.Designer.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.Designer.cs @@ -12,8 +12,8 @@ namespace api_cinema_challenge.Migrations { [DbContext(typeof(CinemaContext))] - [Migration("20250822121753_Working")] - partial class Working + [Migration("20250825130218_UpdateTicket")] + partial class UpdateTicket { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -147,6 +147,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdat"); + b.Property("CustomerId") .HasColumnType("integer"); @@ -157,6 +161,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("ScreeningId") .HasColumnType("integer"); + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + b.HasKey("Id"); b.HasIndex("CustomerId"); diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.cs similarity index 95% rename from api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.cs rename to api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.cs index 48eae7bc..ebc92f0f 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822121753_Working.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.cs @@ -7,7 +7,7 @@ namespace api_cinema_challenge.Migrations { /// - public partial class Working : Migration + public partial class UpdateTicket : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -79,7 +79,9 @@ protected override void Up(MigrationBuilder migrationBuilder) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), numseats = table.Column(type: "integer", nullable: false), CustomerId = table.Column(type: "integer", nullable: false), - ScreeningId = table.Column(type: "integer", nullable: false) + ScreeningId = table.Column(type: "integer", nullable: false), + createdat = table.Column(type: "timestamp with time zone", nullable: false), + updatedat = table.Column(type: "timestamp with time zone", nullable: false) }, constraints: table => { diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs index 1f2539dd..6fec1671 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -144,6 +144,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdat"); + b.Property("CustomerId") .HasColumnType("integer"); @@ -154,6 +158,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ScreeningId") .HasColumnType("integer"); + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + b.HasKey("Id"); b.HasIndex("CustomerId"); diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs index e78f3dfb..ac387ed0 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -17,10 +17,16 @@ public class Ticket public Customer Customer { get; set; } - [ForeignKey("Screening")] + [ForeignKey("Screenings")] public int ScreeningId { get; set; } public Screening Screening { get; set; } + [Column("createdat")] + public DateTime CreatedAt { get; set; } + + [Column("updatedat")] + public DateTime UpdatedAt { get; set; } + } } diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 23d6327d..f6d2c738 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -16,6 +16,7 @@ builder.Services.AddScoped, Repository>(); builder.Services.AddScoped, Repository>(); builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); builder.Services.AddDbContext(options => @@ -42,4 +43,5 @@ app.ConfigureCustomerEndpoints(); app.ConfigureMovieEndpoints(); app.ConfigureScreeningEndpoints(); +app.ConfigureTicketEndpoints(); app.Run(); From 142b34fb9ad32f03408fd37d77bea2b2de8b68e7 Mon Sep 17 00:00:00 2001 From: Snorre Aldstedt Date: Tue, 26 Aug 2025 14:03:35 +0200 Subject: [PATCH 5/5] Add authentication --- .../Controller/UserController.cs | 100 ++++++++++++++ .../DTOs/Requests/AuthRequest.cs | 10 ++ .../DTOs/Requests/RegistrationRequest.cs | 19 +++ .../DTOs/Response/AuthResponse.cs | 10 ++ .../Data/CinemaContext.cs | 10 ++ .../Endpoints/CustomerEndpoint.cs | 6 +- .../Endpoints/MovieEndpoint.cs | 11 +- .../Endpoints/ScreeningEndpoint.cs | 5 +- .../Endpoints/TicketEndpoint.cs | 3 + .../api-cinema-challenge/Enums/Role.cs | 8 ++ .../Helpers/ClaimsPrincipalHelpers.cs | 31 +++++ ...gner.cs => 20250826082751_Jwt.Designer.cs} | 59 ++++++++- ..._UpdateTicket.cs => 20250826082751_Jwt.cs} | 31 ++++- .../Migrations/CinemaContextModelSnapshot.cs | 55 ++++++++ .../Models/ApplicationUser.cs | 12 ++ .../api-cinema-challenge/Program.cs | 122 ++++++++++++++++-- .../Services/TokenService.cs | 84 ++++++++++++ .../api-cinema-challenge.csproj | 6 +- 18 files changed, 562 insertions(+), 20 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Controller/UserController.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Requests/AuthRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Requests/RegistrationRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Response/AuthResponse.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Enums/Role.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelpers.cs rename api-cinema-challenge/api-cinema-challenge/Migrations/{20250825130218_UpdateTicket.Designer.cs => 20250826082751_Jwt.Designer.cs} (80%) rename api-cinema-challenge/api-cinema-challenge/Migrations/{20250825130218_UpdateTicket.cs => 20250826082751_Jwt.cs} (77%) create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs diff --git a/api-cinema-challenge/api-cinema-challenge/Controller/UserController.cs b/api-cinema-challenge/api-cinema-challenge/Controller/UserController.cs new file mode 100644 index 00000000..873ed03a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Controller/UserController.cs @@ -0,0 +1,100 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.DTOs.Requests; +using api_cinema_challenge.DTOs.Response; +using api_cinema_challenge.Enums; +using api_cinema_challenge.Models; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Controller +{ + [ApiController] + [Route("/api/[controller]")] + public class UsersController : ControllerBase + { + private readonly UserManager _userManager; + private readonly CinemaContext _context; + private readonly TokenService _tokenService; + + public UsersController(UserManager userManager, CinemaContext context, + TokenService tokenService, ILogger logger) + { + _userManager = userManager; + _context = context; + _tokenService = tokenService; + } + + + [HttpPost] + [Route("register")] + public async Task Register(RegistrationRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var result = await _userManager.CreateAsync( + new ApplicationUser { UserName = request.Username, Email = request.Email, Role = request.Role }, + request.Password! + ); + + if (result.Succeeded) + { + request.Password = ""; + return CreatedAtAction(nameof(Register), new { email = request.Email, role = Role.User }, request); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(error.Code, error.Description); + } + + return BadRequest(ModelState); + } + + + [HttpPost] + [Route("login")] + public async Task> Authenticate([FromBody] AuthRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var managedUser = await _userManager.FindByEmailAsync(request.Email!); + + if (managedUser == null) + { + return BadRequest("Bad credentials"); + } + + var isPasswordValid = await _userManager.CheckPasswordAsync(managedUser, request.Password!); + + if (!isPasswordValid) + { + return BadRequest("Bad credentials"); + } + + var userInDb = _context.Users.FirstOrDefault(u => u.Email == request.Email); + + if (userInDb is null) + { + return Unauthorized(); + } + + var accessToken = _tokenService.CreateToken(userInDb); + await _context.SaveChangesAsync(); + + return Ok(new AuthResponse + { + Username = userInDb.UserName, + Email = userInDb.Email, + Token = accessToken, + }); + } + } + +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/AuthRequest.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/AuthRequest.cs new file mode 100644 index 00000000..c269f7e8 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/AuthRequest.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs.Requests +{ + public class AuthRequest + { + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() { return true; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/RegistrationRequest.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/RegistrationRequest.cs new file mode 100644 index 00000000..c0a850b2 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/RegistrationRequest.cs @@ -0,0 +1,19 @@ +using api_cinema_challenge.Enums; +using System.ComponentModel.DataAnnotations; + +namespace api_cinema_challenge.DTOs.Requests +{ + public class RegistrationRequest + { + [Required] + public string? Email { get; set; } + + [Required] + public string? Username { get { return this.Email; } set { } } + + [Required] + public string? Password { get; set; } + + public Role Role { get; set; } = Role.User; + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Response/AuthResponse.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Response/AuthResponse.cs new file mode 100644 index 00000000..016e3288 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Response/AuthResponse.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs.Response +{ + public class AuthResponse + { + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index 2061b189..cce2bdef 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; namespace api_cinema_challenge.Data { @@ -42,5 +43,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(s => s.MovieId); } + public DbSet Tickets { get; set; } + public DbSet Users { get; set; } + public DbSet Screenings { get; set; } + public DbSet Customers { get; set; } + public DbSet Movies { get; set; } + + + //public DbSet Ticket { get; set; } + //publi } } diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs index 3df19849..7c5c652b 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -2,6 +2,7 @@ using api_cinema_challenge.DTOs; using api_cinema_challenge.Models; using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -26,6 +27,7 @@ private static CustomerGet CustomerToCustomerGet(Customer c) return customerShow; } + [Authorize(Roles = "Admin,User")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task GetAll(IRepository customerRepo) { List response = new List(); @@ -37,7 +39,7 @@ public static async Task GetAll(IRepository customerRepo) { return TypedResults.Ok(response); } - + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status201Created)] public static async Task Create(IRepository customerRepo, CustomerPost cModel) { @@ -53,6 +55,7 @@ public static async Task Create(IRepository customerRepo, Cus return TypedResults.Created($"Created object with id: {newCustomer.Id}"); } + [Authorize(Roles = "Admin,User")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status404NotFound)] public static async Task Update(IRepository customerRepo, int id, CustomerPost cModel) @@ -72,6 +75,7 @@ public static async Task Update(IRepository customerRepo, int return TypedResults.NotFound(); } + [Authorize(Roles = "Admin,User")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public static async Task Delete(IRepository customerRepo, int id) diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs index d6d2e8da..3dacd158 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -1,6 +1,7 @@ using api_cinema_challenge.DTOs; using api_cinema_challenge.Models; using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Runtime.InteropServices; @@ -25,6 +26,7 @@ public static MovieGet MovieToMovieGet(Movie m) return movieShow; } + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task GetAll(IRepository movieRepo) { @@ -38,6 +40,8 @@ public static async Task GetAll(IRepository movieRepo) return TypedResults.Ok(response); } + [Authorize(Roles = "Admin,User")] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status201Created)] public static async Task Create(IRepository movieRepo, MoviePost mModel) { @@ -55,6 +59,8 @@ public static async Task Create(IRepository movieRepo, MoviePost return TypedResults.Created($"Created object with id: {newMovie.Id}"); } + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status404NotFound)] public static async Task Update(IRepository movieRepo, int id, MoviePost mModel) @@ -70,16 +76,17 @@ public static async Task Update(IRepository movieRepo, int id, M mTarget.RuntimeMins = mModel.RunTimeMins; mTarget.UpdatedAt = time; - - await movieRepo.Update(mTarget); return TypedResults.Created($"Updated object with id: {id}"); } return TypedResults.NotFound(); } + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task Delete(IRepository movieRepo, int id) { Movie? mTarget = await movieRepo.GetById(id); diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs index 6fe3d4d1..3a2a986f 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs @@ -1,6 +1,7 @@ using api_cinema_challenge.DTOs; using api_cinema_challenge.Models; using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace api_cinema_challenge.Endpoints @@ -21,6 +22,7 @@ public static ScreeningGet ScreenToScreeningGet(Screening s) return screenShow; } + [Authorize(Roles = "Admin,User")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task GetAll(IRepository screenRepo, int id) { @@ -34,7 +36,7 @@ public static async Task GetAll(IRepository screenRepo, int } return TypedResults.Ok(response); } - + /* [ProducesResponseType(StatusCodes.Status200OK)] public static async Task GetAll(IRepository screenRepo, int id) @@ -53,6 +55,7 @@ public static async Task GetAll(IRepository screenRepo, int id) return TypedResults.Ok(response); } */ + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status201Created)] public static async Task Create(IRepository screenRepo, ScreeningPost sModel, int id) { diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs index 35609c25..32fb8ffa 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs @@ -1,6 +1,7 @@ using api_cinema_challenge.DTOs; using api_cinema_challenge.Models; using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; namespace api_cinema_challenge.Endpoints @@ -21,6 +22,7 @@ private static TicketGet TicketToTicketGet(Ticket t) return ticketGet; } + [Authorize("Admin,User")] public static async Task GetAll(IRepository ticketRepo, int cId, int sId) { //List results = new List(); @@ -34,6 +36,7 @@ public static async Task GetAll(IRepository ticketRepo, int cId return TypedResults.Ok(resultsShow); } + [Authorize(Roles = "User")] public static async Task Create(IRepository ticketRepo, int cId, int sId, TicketPost t) { DateTime time = DateTime.UtcNow.ToUniversalTime(); diff --git a/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs new file mode 100644 index 00000000..551a6178 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.Enums +{ + public enum Role + { + Admin, + User + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelpers.cs b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelpers.cs new file mode 100644 index 00000000..b1e437f5 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelpers.cs @@ -0,0 +1,31 @@ +using System.Security.Claims; +using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; + +namespace api_cinema_challenge.Helpers +{ + public static class ClaimsPrincipalHelper + { + public static string? UserId(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.NameIdentifier); + return claim?.Value; + } + public static string? Email(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.Email); + return claim?.Value; + } + + // public static string? UserId(this IIdentity identity) + // { + // if (identity != null && identity.IsAuthenticated) + // { + // // return Guid.Parse(((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value); + // return ((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value; + // } + // return null; + // } + } +} + diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.Designer.cs similarity index 80% rename from api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.Designer.cs rename to api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.Designer.cs index 215d9a59..023e98fb 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.Designer.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.Designer.cs @@ -12,8 +12,8 @@ namespace api_cinema_challenge.Migrations { [DbContext(typeof(CinemaContext))] - [Migration("20250825130218_UpdateTicket")] - partial class UpdateTicket + [Migration("20250826082751_Jwt")] + partial class Jwt { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -25,6 +25,61 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("api_cinema_challenge.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => { b.Property("Id") diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.cs similarity index 77% rename from api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.cs rename to api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.cs index ebc92f0f..fa4c8913 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825130218_UpdateTicket.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.cs @@ -7,7 +7,7 @@ namespace api_cinema_challenge.Migrations { /// - public partial class UpdateTicket : Migration + public partial class Jwt : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -47,6 +47,32 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_Movies", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Role = table.Column(type: "integer", nullable: false), + UserName = table.Column(type: "text", nullable: true), + NormalizedUserName = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: true), + NormalizedEmail = table.Column(type: "text", 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_Users", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Screenings", columns: table => new @@ -122,6 +148,9 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "Tickets"); + migrationBuilder.DropTable( + name: "Users"); + migrationBuilder.DropTable( name: "Customers"); diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs index 6fec1671..053c9876 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -22,6 +22,61 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("api_cinema_challenge.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => { b.Property("Id") diff --git a/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs new file mode 100644 index 00000000..8e7e4061 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs @@ -0,0 +1,12 @@ +using api_cinema_challenge.Enums; +using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("Users")] + public class ApplicationUser : IdentityUser + { + public Role Role { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index f6d2c738..369648cc 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -2,46 +2,144 @@ using api_cinema_challenge.Endpoints; using api_cinema_challenge.Models; using api_cinema_challenge.Repository; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; using System.Diagnostics; -using Microsoft.AspNetCore.Mvc; + +using System.Text; +using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddOpenApi(); +builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); + +builder.Services.AddSwaggerGen(option => +{ + option.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); + option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter a valid token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + option.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); +}); + +builder.Services.AddProblemDetails(); +builder.Services.AddApiVersioning(cfg => +{ + cfg.DefaultApiVersion = new ApiVersion(1, 1); + cfg.AssumeDefaultVersionWhenUnspecified = true; +}); +builder.Services.AddRouting(options => options.LowercaseUrls = true); +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); + options.LogTo(message => Debug.WriteLine(message)); +}); + builder.Services.AddDbContext(); builder.Services.AddScoped, Repository>(); builder.Services.AddScoped, Repository>(); builder.Services.AddScoped, Repository>(); builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped(); - -builder.Services.AddDbContext(options => +builder.Services.AddControllers().AddJsonOptions(opt => { - options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); - options.LogTo(message => Debug.WriteLine(message)); + opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); +// Specify identity requirements +// Must be added before .AddAuthentication otherwise a 404 is thrown on authorized endpoints +builder.Services + .AddIdentity(options => + { + options.SignIn.RequireConfirmedAccount = false; + options.User.RequireUniqueEmail = true; + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + }) + .AddRoles() + .AddEntityFrameworkStores(); + +// These will eventually be moved to a secrets file, but for alpha development appsettings is fine +var validIssuer = builder.Configuration.GetValue("JwtTokenSettings:ValidIssuer"); +var validAudience = builder.Configuration.GetValue("JwtTokenSettings:ValidAudience"); +var symmetricSecurityKey = builder.Configuration.GetValue("JwtTokenSettings:SymmetricSecurityKey"); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +}) + .AddJwtBearer(options => + { + options.IncludeErrorDetails = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ClockSkew = TimeSpan.Zero, + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = validIssuer, + ValidAudience = validAudience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + }; + }); + var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.MapOpenApi(); + //app.MapOpenApi(); app.UseSwagger(); - app.UseSwaggerUI( options => - { - options.SwaggerEndpoint("/openapi/v1.json", "Cinema API"); - }); + app.UseSwaggerUI(); } app.UseHttpsRedirection(); +app.UseStatusCodePages(); + + +app.UseAuthentication(); +app.UseAuthorization(); app.ConfigureCustomerEndpoints(); app.ConfigureMovieEndpoints(); app.ConfigureScreeningEndpoints(); app.ConfigureTicketEndpoints(); + +app.MapControllers(); + app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs new file mode 100644 index 00000000..088b30de --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs @@ -0,0 +1,84 @@ +using api_cinema_challenge.Models; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Http.HttpResults; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Identity; + +namespace api_cinema_challenge.Services +{ + public class TokenService + { + private const int ExpirationMinutes = 60; + private readonly ILogger _logger; + public TokenService(ILogger logger) + { + _logger = logger; + } + + public string CreateToken(ApplicationUser user) + { + + var expiration = DateTime.UtcNow.AddMinutes(ExpirationMinutes); + var token = CreateJwtToken( + CreateClaims(user), + CreateSigningCredentials(), + expiration + ); + var tokenHandler = new JwtSecurityTokenHandler(); + + _logger.LogInformation("JWT Token created"); + + return tokenHandler.WriteToken(token); + } + + private JwtSecurityToken CreateJwtToken(List claims, SigningCredentials credentials, + DateTime expiration) => + new( + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidIssuer"], + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidAudience"], + claims, + expires: expiration, + signingCredentials: credentials + ); + + private List CreateClaims(ApplicationUser user) + { + var jwtSub = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["JwtRegisteredClaimNamesSub"]; + + try + { + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, jwtSub), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()) + }; + + return claims; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + private SigningCredentials CreateSigningCredentials() + { + var symmetricSecurityKey = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["SymmetricSecurityKey"]; + + return new SigningCredentials( + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + SecurityAlgorithms.HmacSha256 + ); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj index f56be7d5..82fd9806 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -15,6 +15,10 @@ + + + +