From 03e1cdebb184f9640b26310ba1ee845abc1c36c6 Mon Sep 17 00:00:00 2001 From: Trym Haugan Berger Date: Mon, 25 Aug 2025 10:13:52 +0200 Subject: [PATCH 1/2] base project without JWT completed --- .../api-cinema-challenge/DTOs/CustomerDTO.cs | 9 + .../api-cinema-challenge/DTOs/MovieDTO.cs | 14 + .../api-cinema-challenge/DTOs/ScreeningDTO.cs | 18 ++ .../DTOs/ScreeningPost.cs | 12 + .../api-cinema-challenge/DTOs/TicketDTO.cs | 17 + .../api-cinema-challenge/DTOs/TicketPost.cs | 12 + .../Data/CinemaContext.cs | 89 +++++- .../Endpoints/CinemaEndpoint.cs | 293 ++++++++++++++++++ .../20250822080647_First.Designer.cs | 186 +++++++++++ .../Migrations/20250822080647_First.cs | 116 +++++++ .../20250822084502_SecondImpact.Designer.cs | 211 +++++++++++++ .../Migrations/20250822084502_SecondImpact.cs | 115 +++++++ .../20250825064444_ThirdImpact.Designer.cs | 258 +++++++++++++++ .../Migrations/20250825064444_ThirdImpact.cs | 58 ++++ .../20250825075022_FourthImpact.Designer.cs | 285 +++++++++++++++++ .../Migrations/20250825075022_FourthImpact.cs | 87 ++++++ .../Migrations/CinemaContextModelSnapshot.cs | 282 +++++++++++++++++ .../api-cinema-challenge/Models/Customer.cs | 19 ++ .../api-cinema-challenge/Models/Movie.cs | 27 ++ .../api-cinema-challenge/Models/Screening.cs | 25 ++ .../api-cinema-challenge/Models/Ticket.cs | 24 ++ .../api-cinema-challenge/Program.cs | 16 +- .../Repository/DbEntity.cs | 7 + .../Repository/IRepository.cs | 22 ++ .../api-cinema-challenge/Repository/Repo.cs | 62 ++++ .../api-cinema-challenge.csproj | 6 +- .../appsettings.example.json | 12 - 27 files changed, 2262 insertions(+), 20 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/CustomerDTO.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/MovieDTO.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningDTO.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/TicketDTO.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/TicketPost.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/CinemaEndpoint.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250822080647_First.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250822080647_First.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250822084502_SecondImpact.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250822084502_SecondImpact.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825064444_ThirdImpact.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825064444_ThirdImpact.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825075022_FourthImpact.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825075022_FourthImpact.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.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/Models/Ticket.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/DbEntity.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/Repo.cs delete mode 100644 api-cinema-challenge/api-cinema-challenge/appsettings.example.json diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerDTO.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerDTO.cs new file mode 100644 index 00000000..ba5294be --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerDTO.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs +{ + public class CustomerDTO + { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MovieDTO.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieDTO.cs new file mode 100644 index 00000000..af07ffe0 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieDTO.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.DTOs +{ + public class MovieDTO + { + public string Title { get; set; } + public string Description { get; set; } + public string Rating { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public int RuntimeMins { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningDTO.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningDTO.cs new file mode 100644 index 00000000..46a5c37a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningDTO.cs @@ -0,0 +1,18 @@ +using api_cinema_challenge.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.DTOs +{ + public class ScreeningDTO + { + public int MovieId { get; set; } + public MovieDTO Movie { get; set; } + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime StartsAt { 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..c4107396 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs @@ -0,0 +1,12 @@ +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 CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime StartsAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/TicketDTO.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketDTO.cs new file mode 100644 index 00000000..7979e41b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketDTO.cs @@ -0,0 +1,17 @@ +using api_cinema_challenge.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.DTOs +{ + public class TicketDTO + { + public int NumSeats { get; set; } + public ScreeningDTO Screening { get; set; } + public CustomerDTO Customer { get; set; } + [Column("created_at")] + public DateTime CreatedAt { get; set; } + [Column("updated_at")] + 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..f5ee1f4a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketPost.cs @@ -0,0 +1,12 @@ +using api_cinema_challenge.Models; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.DTOs +{ + public class TicketPost + { + public int NumSeats { get; set; } + public int ScreeningId { get; set; } + public int CustomerId { 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..015103a0 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 @@ -10,17 +11,101 @@ public CinemaContext(DbContextOptions options) : base(options) { var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); _connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString")!; - this.Database.EnsureCreated(); + //this.Database.EnsureCreated(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql(_connectionString); + + } protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity().HasData( + new Customer() { + Id = 1, + Name = "Trym Berger", + Email = "Trym@Berger.no", + Phone = "12459732" + }, + new Customer() + { + Id = 2, + Name = "Monika Synnove", + Email = "mogak@haugan.no", + Phone = "56753212" + } + ); + modelBuilder.Entity().HasData( + new Movie() { + Id = 1, + Title = "Howl's Moving Castle", + Description = "Howl's Moving Castle is a 2004 Japanese animated fantasy film written and directed by Hayao Miyazaki", + Rating = "G", + RuntimeMins = 119, + CreatedAt = DateTime.MinValue, + UpdatedAt = DateTime.MinValue + }, + new Movie() + { + Id = 2, + Title = "Kiki's Delivery Service", + Description = "A 1989 Japanese animated fantasy film written, produced, and directed by Hayao Miyazaki, based on Eiko Kadono's 1985 novel Kiki's Delivery Service.", + Rating = "G", + RuntimeMins = 102, + CreatedAt = DateTime.MinValue, + UpdatedAt = DateTime.MinValue + } + ); + modelBuilder.Entity().HasData( + new Screening() + { + Id = 1, + ScreenNumber = 1, + Capacity = 200, + StartsAt = DateTime.Parse("2025-09-12 14:30").ToUniversalTime(), + MovieId = 1, + CreatedAt = DateTime.MinValue, + UpdatedAt = DateTime.MinValue + }, + new Screening() + { + Id = 2, + ScreenNumber = 2, + Capacity = 130, + StartsAt = DateTime.Parse("2025-09-13 20:30").ToUniversalTime(), + MovieId = 2, + CreatedAt = DateTime.MinValue, + UpdatedAt = DateTime.MinValue + } + ); + modelBuilder.Entity().HasData( + new Ticket() + { + Id = 1, + NumSeats = 1, + ScreeningId = 2, + CustomerId = 1, + CreatedAt = DateTime.Now.ToUniversalTime(), + UpdatedAt = DateTime.Now.ToUniversalTime() + }, + new Ticket() + { + Id = 2, + NumSeats = 3, + ScreeningId = 1, + CustomerId = 2, + CreatedAt = DateTime.Now.ToUniversalTime(), + UpdatedAt= DateTime.Now.ToUniversalTime() + } + ); } + + public DbSet Movies { get; set; } + public DbSet Customers { get; set; } + public DbSet Screenings { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CinemaEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CinemaEndpoint.cs new file mode 100644 index 00000000..50e8edbb --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CinemaEndpoint.cs @@ -0,0 +1,293 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Mvc; +using System.Numerics; +using System.Xml.Linq; + +namespace api_cinema_challenge.Endpoints +{ + public static class CinemaEndpoint + { + public static void ConfigureCinemaEndpoint(this WebApplication app) + { + var cinemaGroup = app.MapGroup("cinema"); + + cinemaGroup.MapPost("/customers", CreateCustomer); + cinemaGroup.MapPut("/customers/{id}", UpdateCustomer); + cinemaGroup.MapDelete("/customers{id}", DeleteCustomer); + cinemaGroup.MapGet("/customers", GetCustomers); + cinemaGroup.MapPost("/movies", CreateMovie); + cinemaGroup.MapPut("/movies/{id}", UpdateMovie); + cinemaGroup.MapDelete("/movies{id}", DeleteMovie); + cinemaGroup.MapGet("/movies", GetMovies); + cinemaGroup.MapGet("/movies/{id}", GetMovieById); + cinemaGroup.MapPost("/screenings", CreateScreening); + cinemaGroup.MapGet("/screenings", GetScreenings); + cinemaGroup.MapPost("/tickets", CreateTicket); + cinemaGroup.MapGet("/tickets", GetTickets); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetMovies(IRepository repository) + { + List movies = new List(); + var allMovies = await repository.GetAll(); + + foreach (var movie in allMovies) + { + movies.Add(new MovieDTO { Title = movie.Title, Description = movie.Description, Rating = movie.Rating, + RuntimeMins = movie.RuntimeMins, CreatedAt = movie.CreatedAt, UpdatedAt = movie.UpdatedAt}); + } + + return TypedResults.Ok(movies); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetTickets(IRepository repository, IRepository screenRepo, IRepository customerRepo) + { + List tickets = new List(); + var allTickets = await repository.GetAll(); + + foreach (var ticket in allTickets) + { + Screening screening = await screenRepo.GetByIdWithIncludes(ticket => ticket.Movie, ticket.ScreeningId); + Customer cust = await customerRepo.GetById(ticket.CustomerId); + ScreeningDTO screendto = new ScreeningDTO + { + MovieId = screening.MovieId, + ScreenNumber = screening.ScreenNumber, + Capacity = screening.Capacity, + CreatedAt = screening.CreatedAt, + UpdatedAt = screening.UpdatedAt, + StartsAt = screening.StartsAt, + Movie = new MovieDTO + { + Title = screening.Movie.Title, + Description = screening.Movie.Description, + Rating = screening.Movie.Rating, + RuntimeMins = screening.Movie.RuntimeMins, + CreatedAt = screening.Movie.CreatedAt, + UpdatedAt = screening.Movie.UpdatedAt + } + }; + CustomerDTO customerdto = new CustomerDTO { + Name = cust.Name, + Email = cust.Email, + Phone = cust.Phone + }; + + tickets.Add(new TicketDTO + { + NumSeats = ticket.NumSeats, + Screening = screendto, + Customer = customerdto, + CreatedAt = ticket.CreatedAt, + UpdatedAt = ticket.UpdatedAt + }); + } + + return TypedResults.Ok(tickets); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetCustomers(IRepository repository) + { + List customerDTOs = new List(); + var customers = await repository.GetAll(); + + foreach (var customer in customers) + { + customerDTOs.Add(new CustomerDTO + { + Name = customer.Name, + Email = customer.Email, + Phone = customer.Phone + }); + } + + return TypedResults.Ok(customerDTOs); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task CreateCustomer(IRepository repository, CustomerDTO cust) + { + Customer newcust = new Customer { Name = cust.Name, Email = cust.Email, Phone = cust.Phone }; + newcust = await repository.Create(newcust); + return TypedResults.Created("Created new customer", new CustomerDTO { Name = newcust.Name, Email = newcust.Email, Phone = newcust.Phone }); + } + + + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task CreateTicket(IRepository repository, TicketPost ticket) + { + Ticket newticket = new Ticket { NumSeats = ticket.NumSeats, CustomerId = ticket.CustomerId, ScreeningId = ticket.ScreeningId, + CreatedAt = DateTime.Now.ToUniversalTime(), UpdatedAt = DateTime.Now.ToUniversalTime() }; + newticket = await repository.Create(newticket); + return TypedResults.Created("Success", new { + NumSeats = newticket.NumSeats, + CreatedAt = newticket.CreatedAt, + UpdatedAt = newticket.UpdatedAt + }); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task CreateScreening(IRepository repository, ScreeningPost screen) + { + Screening newscreen = new Screening { MovieId = screen.MovieId, ScreenNumber = screen.ScreenNumber, Capacity = screen.Capacity, CreatedAt = screen.CreatedAt, UpdatedAt = screen.UpdatedAt, StartsAt = screen.StartsAt }; + newscreen = await repository.Create(newscreen); + return TypedResults.Created("Created new screening", new ScreeningDTO { MovieId = newscreen.MovieId, ScreenNumber = newscreen.ScreenNumber, Capacity = newscreen.Capacity, CreatedAt = newscreen.CreatedAt, UpdatedAt = newscreen.UpdatedAt, StartsAt = newscreen.StartsAt }); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetScreenings(IRepository repository) + { + List screeningDTOs = new List(); + var screenings = await repository.GetAllWithIncludes(s => s.Movie); + + foreach (var screening in screenings) + { + screeningDTOs.Add(new ScreeningDTO + { + MovieId = screening.MovieId, + ScreenNumber = screening.ScreenNumber, + Capacity = screening.Capacity, + CreatedAt = screening.CreatedAt, + UpdatedAt = screening.UpdatedAt, + StartsAt = screening.StartsAt, + Movie = new MovieDTO + { + Title = screening.Movie.Title, + Description = screening.Movie.Description, + Rating = screening.Movie.Rating, + RuntimeMins = screening.Movie.RuntimeMins, + CreatedAt = screening.Movie.CreatedAt, + UpdatedAt = screening.Movie.UpdatedAt + } + }); + } + + return TypedResults.Ok(screeningDTOs); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task CreateMovie(IRepository repository, MovieDTO mov) + { + Movie newmov = new Movie + { + Title = mov.Title, + Description = mov.Description, + Rating = mov.Rating, + RuntimeMins = mov.RuntimeMins, + CreatedAt = mov.CreatedAt, + UpdatedAt = mov.UpdatedAt + }; + newmov = await repository.Create(newmov); + return TypedResults.Created("Created new customer", new MovieDTO + { + Title = mov.Title, + Description = mov.Description, + Rating = mov.Rating, + RuntimeMins = mov.RuntimeMins, + CreatedAt = mov.CreatedAt, + UpdatedAt = mov.UpdatedAt + }); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetMovieById(IRepository repository, int id) + { + Movie mov = await repository.GetById(id); + MovieDTO movdto = new MovieDTO + { + Title = mov.Title, + Description = mov.Description, + Rating = mov.Rating, + RuntimeMins = mov.RuntimeMins, + CreatedAt = mov.CreatedAt, + UpdatedAt = mov.UpdatedAt + }; + return TypedResults.Ok(movdto); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task DeleteMovie(IRepository repository, int id) + { + Movie mov = await repository.Delete(id); + MovieDTO movdto = new MovieDTO + { + Title = mov.Title, + Description = mov.Description, + Rating = mov.Rating, + RuntimeMins = mov.RuntimeMins, + CreatedAt = mov.CreatedAt, + UpdatedAt = mov.UpdatedAt + }; + return TypedResults.Ok(movdto); + } + + public static async Task DeleteCustomer(IRepository repository, int id) + { + Customer cust = await repository.Delete(id); + CustomerDTO custdto = new CustomerDTO + { + Name = cust.Name, + Email = cust.Email, + Phone = cust.Phone + }; + return TypedResults.Ok(custdto); + } + + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task Delete(IRepository repository, int id) + { + Movie mov = await repository.Delete(id); + MovieDTO movdto = new MovieDTO + { + Title = mov.Title, + Description = mov.Description, + Rating = mov.Rating, + RuntimeMins = mov.RuntimeMins, + CreatedAt = mov.CreatedAt, + UpdatedAt = mov.UpdatedAt + }; + return TypedResults.Ok(movdto); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task UpdateCustomer(IRepository repository, CustomerDTO custdto, int id) + { + Customer cust = new Customer {Name = custdto.Name, Email = custdto.Email, Phone = custdto.Phone}; + var updatedCust = await repository.Update(cust, id); + CustomerDTO returnedCust = new CustomerDTO { Name = updatedCust.Name, Email = updatedCust.Email, Phone = updatedCust.Phone }; + return TypedResults.Ok(returnedCust); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task UpdateMovie(IRepository repository, MovieDTO movdto, int id) + { + Movie mov = new Movie + { + Title = movdto.Title, + Description = movdto.Description, + Rating = movdto.Rating, + RuntimeMins = movdto.RuntimeMins, + CreatedAt = movdto.CreatedAt, + UpdatedAt = DateTime.Now + }; + var updatedMov = await repository.Update(mov, id); + MovieDTO returnedMov = new MovieDTO + { + Title = updatedMov.Title, + Description = updatedMov.Description, + Rating = updatedMov.Rating, + RuntimeMins = updatedMov.RuntimeMins, + CreatedAt = updatedMov.CreatedAt, + UpdatedAt = updatedMov.UpdatedAt + }; + + return TypedResults.Ok(returnedMov); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822080647_First.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822080647_First.Designer.cs new file mode 100644 index 00000000..c5d84168 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822080647_First.Designer.cs @@ -0,0 +1,186 @@ +// +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(Data.CinemaContext))] + [Migration("20250822080647_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("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + Email = "Trym@Berger.no", + Name = "Trym Berger", + Phone = "12459732" + }, + new + { + Id = 2, + Email = "mogak@haugan.no", + Name = "Monika Synnove", + Phone = "56753212" + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("runtimeMins") + .HasColumnType("integer") + .HasColumnName("runtime_minutes"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + Description = "Howl's Moving Castle is a 2004 Japanese animated fantasy film written and directed by Hayao Miyazaki", + Rating = "Very good", + Title = "Howl's Moving Castle", + runtimeMins = 119 + }, + new + { + Id = 2, + Description = "A 1989 Japanese animated fantasy film written, produced, and directed by Hayao Miyazaki, based on Eiko Kadono's 1985 novel Kiki's Delivery Service.", + Rating = "Very good", + Title = "Kiki's Delivery Service", + runtimeMins = 102 + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screen_number"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 200, + MovieId = 1, + ScreenNumber = 1, + StartsAt = new DateTime(2025, 9, 12, 12, 30, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + Capacity = 130, + MovieId = 2, + ScreenNumber = 2, + StartsAt = new DateTime(2025, 9, 13, 18, 30, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.Movie", b => + { + b.Navigation("Screenings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822080647_First.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822080647_First.cs new file mode 100644 index 00000000..cbcd0400 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822080647_First.cs @@ -0,0 +1,116 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +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) + }, + 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), + description = table.Column(type: "text", nullable: false), + rating = table.Column(type: "text", nullable: false), + runtime_minutes = table.Column(type: "integer", 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), + screen_number = table.Column(type: "integer", nullable: false), + Capacity = table.Column(type: "integer", nullable: false), + StartsAt = 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.InsertData( + table: "customers", + columns: new[] { "Id", "email", "name", "phone" }, + values: new object[,] + { + { 1, "Trym@Berger.no", "Trym Berger", "12459732" }, + { 2, "mogak@haugan.no", "Monika Synnove", "56753212" } + }); + + migrationBuilder.InsertData( + table: "movies", + columns: new[] { "Id", "description", "rating", "title", "runtime_minutes" }, + values: new object[,] + { + { 1, "Howl's Moving Castle is a 2004 Japanese animated fantasy film written and directed by Hayao Miyazaki", "Very good", "Howl's Moving Castle", 119 }, + { 2, "A 1989 Japanese animated fantasy film written, produced, and directed by Hayao Miyazaki, based on Eiko Kadono's 1985 novel Kiki's Delivery Service.", "Very good", "Kiki's Delivery Service", 102 } + }); + + migrationBuilder.InsertData( + table: "screenings", + columns: new[] { "Id", "Capacity", "MovieId", "screen_number", "StartsAt" }, + values: new object[,] + { + { 1, 200, 1, 1, new DateTime(2025, 9, 12, 12, 30, 0, 0, DateTimeKind.Utc) }, + { 2, 130, 2, 2, new DateTime(2025, 9, 13, 18, 30, 0, 0, DateTimeKind.Utc) } + }); + + migrationBuilder.CreateIndex( + name: "IX_screenings_MovieId", + table: "screenings", + column: "MovieId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "customers"); + + migrationBuilder.DropTable( + name: "screenings"); + + migrationBuilder.DropTable( + name: "movies"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822084502_SecondImpact.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822084502_SecondImpact.Designer.cs new file mode 100644 index 00000000..fc393167 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822084502_SecondImpact.Designer.cs @@ -0,0 +1,211 @@ +// +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(Data.CinemaContext))] + [Migration("20250822084502_SecondImpact")] + partial class SecondImpact + { + /// + 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("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + Email = "Trym@Berger.no", + Name = "Trym Berger", + Phone = "12459732" + }, + new + { + Id = 2, + Email = "mogak@haugan.no", + Name = "Monika Synnove", + Phone = "56753212" + }); + }); + + 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("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("runtimeMins") + .HasColumnType("integer") + .HasColumnName("runtime_minutes"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Howl's Moving Castle is a 2004 Japanese animated fantasy film written and directed by Hayao Miyazaki", + Rating = "G", + Title = "Howl's Moving Castle", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + runtimeMins = 119 + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "A 1989 Japanese animated fantasy film written, produced, and directed by Hayao Miyazaki, based on Eiko Kadono's 1985 novel Kiki's Delivery Service.", + Rating = "G", + Title = "Kiki's Delivery Service", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + runtimeMins = 102 + }); + }); + + 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("created_at"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screen_number"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 200, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 1, + ScreenNumber = 1, + StartsAt = new DateTime(2025, 9, 12, 12, 30, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Capacity = 130, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 2, + ScreenNumber = 2, + StartsAt = new DateTime(2025, 9, 13, 18, 30, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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.Movie", b => + { + b.Navigation("Screenings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822084502_SecondImpact.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822084502_SecondImpact.cs new file mode 100644 index 00000000..cc7a2a1a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822084502_SecondImpact.cs @@ -0,0 +1,115 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class SecondImpact : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Capacity", + table: "screenings", + newName: "capacity"); + + migrationBuilder.AddColumn( + name: "created_at", + table: "screenings", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "updated_at", + table: "screenings", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "created_at", + table: "movies", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "updated_at", + table: "movies", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.UpdateData( + table: "movies", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "created_at", "rating", "updated_at" }, + values: new object[] { new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), "G", new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) }); + + migrationBuilder.UpdateData( + table: "movies", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "created_at", "rating", "updated_at" }, + values: new object[] { new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), "G", new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) }); + + migrationBuilder.UpdateData( + table: "screenings", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "created_at", "updated_at" }, + values: new object[] { new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) }); + + migrationBuilder.UpdateData( + table: "screenings", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "created_at", "updated_at" }, + values: new object[] { new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "created_at", + table: "screenings"); + + migrationBuilder.DropColumn( + name: "updated_at", + table: "screenings"); + + migrationBuilder.DropColumn( + name: "created_at", + table: "movies"); + + migrationBuilder.DropColumn( + name: "updated_at", + table: "movies"); + + migrationBuilder.RenameColumn( + name: "capacity", + table: "screenings", + newName: "Capacity"); + + migrationBuilder.UpdateData( + table: "movies", + keyColumn: "Id", + keyValue: 1, + column: "rating", + value: "Very good"); + + migrationBuilder.UpdateData( + table: "movies", + keyColumn: "Id", + keyValue: 2, + column: "rating", + value: "Very good"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064444_ThirdImpact.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064444_ThirdImpact.Designer.cs new file mode 100644 index 00000000..66e28611 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064444_ThirdImpact.Designer.cs @@ -0,0 +1,258 @@ +// +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("20250825064444_ThirdImpact")] + partial class ThirdImpact + { + /// + 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("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + Email = "Trym@Berger.no", + Name = "Trym Berger", + Phone = "12459732" + }, + new + { + Id = 2, + Email = "mogak@haugan.no", + Name = "Monika Synnove", + Phone = "56753212" + }); + }); + + 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("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("runtime_minutes"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Howl's Moving Castle is a 2004 Japanese animated fantasy film written and directed by Hayao Miyazaki", + Rating = "G", + RuntimeMins = 119, + Title = "Howl's Moving Castle", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "A 1989 Japanese animated fantasy film written, produced, and directed by Hayao Miyazaki, based on Eiko Kadono's 1985 novel Kiki's Delivery Service.", + Rating = "G", + RuntimeMins = 102, + Title = "Kiki's Delivery Service", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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("created_at"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screen_number"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 200, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 1, + ScreenNumber = 1, + StartsAt = new DateTime(2025, 9, 12, 12, 30, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Capacity = 130, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 2, + ScreenNumber = 2, + StartsAt = new DateTime(2025, 9, 13, 18, 30, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("NumSeats") + .HasColumnType("integer") + .HasColumnName("number_of_seats"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ScreeningId"); + + b.ToTable("tickets"); + + b.HasData( + new + { + Id = 1, + NumSeats = 1, + ScreeningId = 2 + }, + new + { + Id = 2, + NumSeats = 3, + ScreeningId = 1 + }); + }); + + 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.Screening", "Screening") + .WithMany() + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064444_ThirdImpact.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064444_ThirdImpact.cs new file mode 100644 index 00000000..c36e0c27 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064444_ThirdImpact.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class ThirdImpact : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "tickets", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + number_of_seats = 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_screenings_ScreeningId", + column: x => x.ScreeningId, + principalTable: "screenings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "tickets", + columns: new[] { "Id", "number_of_seats", "ScreeningId" }, + values: new object[,] + { + { 1, 1, 2 }, + { 2, 3, 1 } + }); + + migrationBuilder.CreateIndex( + name: "IX_tickets_ScreeningId", + table: "tickets", + column: "ScreeningId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "tickets"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825075022_FourthImpact.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825075022_FourthImpact.Designer.cs new file mode 100644 index 00000000..00a85471 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825075022_FourthImpact.Designer.cs @@ -0,0 +1,285 @@ +// +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("20250825075022_FourthImpact")] + partial class FourthImpact + { + /// + 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("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + Email = "Trym@Berger.no", + Name = "Trym Berger", + Phone = "12459732" + }, + new + { + Id = 2, + Email = "mogak@haugan.no", + Name = "Monika Synnove", + Phone = "56753212" + }); + }); + + 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("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("runtime_minutes"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Howl's Moving Castle is a 2004 Japanese animated fantasy film written and directed by Hayao Miyazaki", + Rating = "G", + RuntimeMins = 119, + Title = "Howl's Moving Castle", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "A 1989 Japanese animated fantasy film written, produced, and directed by Hayao Miyazaki, based on Eiko Kadono's 1985 novel Kiki's Delivery Service.", + Rating = "G", + RuntimeMins = 102, + Title = "Kiki's Delivery Service", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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("created_at"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screen_number"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 200, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 1, + ScreenNumber = 1, + StartsAt = new DateTime(2025, 9, 12, 12, 30, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Capacity = 130, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 2, + ScreenNumber = 2, + StartsAt = new DateTime(2025, 9, 13, 18, 30, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer") + .HasColumnName("number_of_seats"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 1, + NumSeats = 1, + ScreeningId = 2, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 2, + NumSeats = 3, + ScreeningId = 1, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany() + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825075022_FourthImpact.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825075022_FourthImpact.cs new file mode 100644 index 00000000..8b634f2b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825075022_FourthImpact.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class FourthImpact : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CustomerId", + table: "tickets", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "created_at", + table: "tickets", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "updated_at", + table: "tickets", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.UpdateData( + table: "tickets", + keyColumn: "Id", + keyValue: 1, + columns: new[] { "created_at", "CustomerId", "updated_at" }, + values: new object[] { new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), 1, new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) }); + + migrationBuilder.UpdateData( + table: "tickets", + keyColumn: "Id", + keyValue: 2, + columns: new[] { "created_at", "CustomerId", "updated_at" }, + values: new object[] { new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), 2, new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) }); + + migrationBuilder.CreateIndex( + name: "IX_tickets_CustomerId", + table: "tickets", + column: "CustomerId"); + + migrationBuilder.AddForeignKey( + name: "FK_tickets_customers_CustomerId", + table: "tickets", + column: "CustomerId", + principalTable: "customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_tickets_customers_CustomerId", + table: "tickets"); + + migrationBuilder.DropIndex( + name: "IX_tickets_CustomerId", + table: "tickets"); + + migrationBuilder.DropColumn( + name: "CustomerId", + table: "tickets"); + + migrationBuilder.DropColumn( + name: "created_at", + table: "tickets"); + + migrationBuilder.DropColumn( + name: "updated_at", + table: "tickets"); + } + } +} 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..8e86d56e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,282 @@ +// +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("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + Email = "Trym@Berger.no", + Name = "Trym Berger", + Phone = "12459732" + }, + new + { + Id = 2, + Email = "mogak@haugan.no", + Name = "Monika Synnove", + Phone = "56753212" + }); + }); + + 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("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("runtime_minutes"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Howl's Moving Castle is a 2004 Japanese animated fantasy film written and directed by Hayao Miyazaki", + Rating = "G", + RuntimeMins = 119, + Title = "Howl's Moving Castle", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "A 1989 Japanese animated fantasy film written, produced, and directed by Hayao Miyazaki, based on Eiko Kadono's 1985 novel Kiki's Delivery Service.", + Rating = "G", + RuntimeMins = 102, + Title = "Kiki's Delivery Service", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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("created_at"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screen_number"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 200, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 1, + ScreenNumber = 1, + StartsAt = new DateTime(2025, 9, 12, 12, 30, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Capacity = 130, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 2, + ScreenNumber = 2, + StartsAt = new DateTime(2025, 9, 13, 18, 30, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer") + .HasColumnName("number_of_seats"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 1, + NumSeats = 1, + ScreeningId = 2, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 2, + NumSeats = 3, + ScreeningId = 1, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany() + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); +#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 new file mode 100644 index 00000000..885be93a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,19 @@ +using api_cinema_challenge.Repository; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("customers")] + public class Customer : DbEntity + { + [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; } + } +} 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..c44a75ab --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,27 @@ +using api_cinema_challenge.Repository; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("movies")] + public class Movie : DbEntity + { + [Key] + public int Id { get; set; } + [Column("title")] + public string Title { get; set; } + [Column("description")] + public string Description { get; set; } + [Column("rating")] + public string Rating { get; set; } + [Column("runtime_minutes")] + public int RuntimeMins { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + public List Screenings { 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..391d19f2 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,25 @@ +using api_cinema_challenge.Repository; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("screenings")] + public class Screening : DbEntity + { + [Key] + public int Id { get; set; } + [Column("screen_number")] + public int ScreenNumber { get; set; } + [Column("capacity")] + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + [Column("created_at")] + public DateTime CreatedAt { get; set; } + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + + public int MovieId { get; set; } + public Movie Movie { get; set; } + } +} 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..7ef0a10f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,24 @@ +using api_cinema_challenge.Repository; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("tickets")] + public class Ticket : DbEntity + { + [Key] + public int Id { get; set; } + [Column("number_of_seats")] + public int NumSeats { get; set; } + [Column("created_at")] + public DateTime CreatedAt { get; set; } + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + public int ScreeningId { get; set; } + public Screening Screening { get; set; } + public int CustomerId { get; set; } + public Customer Customer { get; set; } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..2ce4bfef 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,11 +1,24 @@ 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; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddDbContext(); +builder.Services.AddDbContext(options => { + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); + options.LogTo(message => Debug.WriteLine(message)); +}); + +builder.Services.AddScoped(typeof(IRepository), typeof(Repo)); +builder.Services.AddScoped(typeof(IRepository), typeof(Repo)); +builder.Services.AddScoped(typeof(IRepository), typeof(Repo)); +builder.Services.AddScoped(typeof(IRepository), typeof(Repo)); var app = builder.Build(); @@ -17,4 +30,5 @@ } app.UseHttpsRedirection(); +app.ConfigureCinemaEndpoint(); app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/DbEntity.cs b/api-cinema-challenge/api-cinema-challenge/Repository/DbEntity.cs new file mode 100644 index 00000000..287b3329 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/DbEntity.cs @@ -0,0 +1,7 @@ +namespace api_cinema_challenge.Repository +{ + public class DbEntity + { + public int Id { 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..8d89da8d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs @@ -0,0 +1,22 @@ +using api_cinema_challenge.Models; +using System.Linq.Expressions; + +namespace api_cinema_challenge.Repository +{ + public interface IRepository where T : class + { + Task> GetAll(); + + Task GetById(int id); + + Task Create(T newObj); + + Task Delete(int id); + + Task Update(T upObj, int id); + + Task> GetAllWithIncludes(Expression> func); + + Task GetByIdWithIncludes(Expression> func, int id); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Repo.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Repo.cs new file mode 100644 index 00000000..28485dd8 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Repo.cs @@ -0,0 +1,62 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq.Expressions; + +namespace api_cinema_challenge.Repository +{ + public class Repo : IRepository where T : DbEntity + { + protected readonly CinemaContext _db; + public Repo(CinemaContext db) + { + _db = db; + } + + public async Task Create(T newobj) + { + T added = _db.Add(newobj).Entity; + await _db.SaveChangesAsync(); + return added; + } + + public async Task Delete(int id) + { + T delObj = await this.GetById(id); + _db.Remove(delObj); + await _db.SaveChangesAsync(); + return delObj; + } + + public async Task> GetAll() + { + return await _db.Set().ToListAsync(); + } + + public async Task GetById(int id) + { + return _db.Set().Find(id); + } + + public async Task Update(T upObj, int id) + { + T upd = await this.GetById(id); + _db.Update(upd); + upd = upObj; + await _db.SaveChangesAsync(); + return upd; + } + + public async Task> GetAllWithIncludes(Expression> func) + { + return await _db.Set().Include(func).ToListAsync(); + } + + public async Task GetByIdWithIncludes(Expression> func, int id) + { + return await _db.Set().Include(func).Where(t => t.Id == id).FirstOrDefaultAsync(); + } + } +} + 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..ebb36b31 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 @@ -27,8 +27,4 @@ - - - - diff --git a/api-cinema-challenge/api-cinema-challenge/appsettings.example.json b/api-cinema-challenge/api-cinema-challenge/appsettings.example.json deleted file mode 100644 index b9175fe6..00000000 --- a/api-cinema-challenge/api-cinema-challenge/appsettings.example.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "ConnectionStrings": { - "DefaultConnectionString": "Host=HOST; Database=DATABASE; Username=USERNAME; Password=PASSWORD;" - } -} \ No newline at end of file From ecb416eeeb75a9b26bdb7566a138a4c6d25b5298 Mon Sep 17 00:00:00 2001 From: Trym Haugan Berger Date: Mon, 25 Aug 2025 15:42:00 +0200 Subject: [PATCH 2/2] added jwt --- .../Controllers/UserController.cs | 101 ++++++ .../Data/CinemaContext.cs | 9 +- .../DataTransfer/Requests/AuthRequest.cs | 12 + .../Requests/RegistrationRequest.cs | 18 + .../DataTransfer/Response/AuthResponse.cs | 9 + .../Endpoints/CinemaEndpoint.cs | 26 +- .../api-cinema-challenge/Enums/Role.cs | 7 + .../Helpers/ClaimsPrincipalHelper.cs | 30 ++ .../20250825105724_FirstJWT.Designer.cs | 340 ++++++++++++++++++ .../Migrations/20250825105724_FirstJWT.cs | 48 +++ .../Migrations/CinemaContextModelSnapshot.cs | 55 +++ .../Models/ApplicationUser.cs | 10 + .../api-cinema-challenge/Program.cs | 97 ++++- .../Services/TokenService.cs | 84 +++++ .../api-cinema-challenge.csproj | 12 +- 15 files changed, 829 insertions(+), 29 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/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/ClaimsPrincipalHelper.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825105724_FirstJWT.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825105724_FirstJWT.cs 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/Controllers/UserController.cs b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs new file mode 100644 index 00000000..306d2297 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Data; +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.DataTransfer.Requests; +using api_cinema_challenge.DataTransfer.Response; +using api_cinema_challenge.Enums; +using api_cinema_challenge.Services; + +namespace api_cinema_challenge.Controllers +{ + + [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, + }); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index 015103a0..7a6365d3 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -89,8 +89,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) NumSeats = 1, ScreeningId = 2, CustomerId = 1, - CreatedAt = DateTime.Now.ToUniversalTime(), - UpdatedAt = DateTime.Now.ToUniversalTime() + CreatedAt = DateTime.MinValue, + UpdatedAt = DateTime.MinValue }, new Ticket() { @@ -98,8 +98,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) NumSeats = 3, ScreeningId = 1, CustomerId = 2, - CreatedAt = DateTime.Now.ToUniversalTime(), - UpdatedAt= DateTime.Now.ToUniversalTime() + CreatedAt = DateTime.MinValue, + UpdatedAt= DateTime.MinValue } ); } @@ -107,5 +107,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public DbSet Movies { get; set; } public DbSet Customers { get; set; } public DbSet Screenings { get; set; } + public DbSet Users { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs new file mode 100644 index 00000000..8fb6640e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.DataTransfer.Requests; + +public class AuthRequest +{ + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() + { + return true; + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs new file mode 100644 index 00000000..cda10dd4 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs @@ -0,0 +1,18 @@ + +using System.ComponentModel.DataAnnotations; +using api_cinema_challenge.Enums; + + +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; +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs new file mode 100644 index 00000000..84b0c057 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DataTransfer.Response; + + +public class AuthResponse +{ + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CinemaEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CinemaEndpoint.cs index 50e8edbb..229fe7b2 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/CinemaEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CinemaEndpoint.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.Numerics; using System.Xml.Linq; @@ -28,6 +29,7 @@ public static void ConfigureCinemaEndpoint(this WebApplication app) cinemaGroup.MapGet("/tickets", GetTickets); } + [Authorize(Roles = "User")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task GetMovies(IRepository repository) { @@ -42,7 +44,7 @@ public static async Task GetMovies(IRepository repository) return TypedResults.Ok(movies); } - + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task GetTickets(IRepository repository, IRepository screenRepo, IRepository customerRepo) { @@ -89,7 +91,7 @@ public static async Task GetTickets(IRepository repository, IRe return TypedResults.Ok(tickets); } - + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task GetCustomers(IRepository repository) { @@ -108,7 +110,7 @@ public static async Task GetCustomers(IRepository repository) return TypedResults.Ok(customerDTOs); } - + [Authorize(Roles = "User")] [ProducesResponseType(StatusCodes.Status201Created)] public static async Task CreateCustomer(IRepository repository, CustomerDTO cust) { @@ -130,7 +132,7 @@ public static async Task CreateTicket(IRepository repository, T UpdatedAt = newticket.UpdatedAt }); } - + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status201Created)] public static async Task CreateScreening(IRepository repository, ScreeningPost screen) { @@ -138,7 +140,7 @@ public static async Task CreateScreening(IRepository reposit newscreen = await repository.Create(newscreen); return TypedResults.Created("Created new screening", new ScreeningDTO { MovieId = newscreen.MovieId, ScreenNumber = newscreen.ScreenNumber, Capacity = newscreen.Capacity, CreatedAt = newscreen.CreatedAt, UpdatedAt = newscreen.UpdatedAt, StartsAt = newscreen.StartsAt }); } - + [Authorize(Roles = "User")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task GetScreenings(IRepository repository) { @@ -169,7 +171,7 @@ public static async Task GetScreenings(IRepository repositor return TypedResults.Ok(screeningDTOs); } - + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status201Created)] public static async Task CreateMovie(IRepository repository, MovieDTO mov) { @@ -193,7 +195,7 @@ public static async Task CreateMovie(IRepository repository, Mov UpdatedAt = mov.UpdatedAt }); } - + [Authorize(Roles = "User")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task GetMovieById(IRepository repository, int id) { @@ -209,7 +211,7 @@ public static async Task GetMovieById(IRepository repository, in }; return TypedResults.Ok(movdto); } - + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task DeleteMovie(IRepository repository, int id) { @@ -225,7 +227,7 @@ public static async Task DeleteMovie(IRepository repository, int }; return TypedResults.Ok(movdto); } - + [Authorize(Roles = "Admin")] public static async Task DeleteCustomer(IRepository repository, int id) { Customer cust = await repository.Delete(id); @@ -238,7 +240,7 @@ public static async Task DeleteCustomer(IRepository repositor return TypedResults.Ok(custdto); } - + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task Delete(IRepository repository, int id) { @@ -254,7 +256,7 @@ public static async Task Delete(IRepository repository, int id) }; return TypedResults.Ok(movdto); } - + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task UpdateCustomer(IRepository repository, CustomerDTO custdto, int id) { @@ -263,7 +265,7 @@ public static async Task UpdateCustomer(IRepository repositor CustomerDTO returnedCust = new CustomerDTO { Name = updatedCust.Name, Email = updatedCust.Email, Phone = updatedCust.Phone }; return TypedResults.Ok(returnedCust); } - + [Authorize(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public static async Task UpdateMovie(IRepository repository, MovieDTO movdto, int id) { 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..f947532c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs @@ -0,0 +1,7 @@ +namespace api_cinema_challenge.Enums; + +public enum Role +{ + Admin, + User +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs new file mode 100644 index 00000000..a01d1dec --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; +using System.Security.Claims; + +namespace api_cinema_challenge.webapi +{ + 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/20250825105724_FirstJWT.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105724_FirstJWT.Designer.cs new file mode 100644 index 00000000..042d4665 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105724_FirstJWT.Designer.cs @@ -0,0 +1,340 @@ +// +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("20250825105724_FirstJWT")] + partial class FirstJWT + { + /// + 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.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") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + Email = "Trym@Berger.no", + Name = "Trym Berger", + Phone = "12459732" + }, + new + { + Id = 2, + Email = "mogak@haugan.no", + Name = "Monika Synnove", + Phone = "56753212" + }); + }); + + 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("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("runtime_minutes"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Howl's Moving Castle is a 2004 Japanese animated fantasy film written and directed by Hayao Miyazaki", + Rating = "G", + RuntimeMins = 119, + Title = "Howl's Moving Castle", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "A 1989 Japanese animated fantasy film written, produced, and directed by Hayao Miyazaki, based on Eiko Kadono's 1985 novel Kiki's Delivery Service.", + Rating = "G", + RuntimeMins = 102, + Title = "Kiki's Delivery Service", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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("created_at"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screen_number"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 200, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 1, + ScreenNumber = 1, + StartsAt = new DateTime(2025, 9, 12, 12, 30, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Capacity = 130, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 2, + ScreenNumber = 2, + StartsAt = new DateTime(2025, 9, 13, 18, 30, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer") + .HasColumnName("number_of_seats"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 1, + NumSeats = 1, + ScreeningId = 2, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 2, + NumSeats = 3, + ScreeningId = 1, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany() + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105724_FirstJWT.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105724_FirstJWT.cs new file mode 100644 index 00000000..1671b6b0 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105724_FirstJWT.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class FirstJWT : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + 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); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs index 8e86d56e..c0686e0d 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..6b59b648 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs @@ -0,0 +1,10 @@ +using api_cinema_challenge.Enums; +using Microsoft.AspNetCore.Identity; + +namespace api_cinema_challenge.Models +{ + 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 2ce4bfef..aaef0356 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -2,24 +2,104 @@ 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.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; using System.Diagnostics; +using System.Text; +using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -builder.Services.AddDbContext(options => { - options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); - options.LogTo(message => Debug.WriteLine(message)); +builder.Services.AddControllers(); +builder.Services.AddSwaggerGen(option => +{ + option.SwaggerDoc("v1", new OpenApiInfo { Title = "Cinema 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(); +builder.Services.AddRouting(options => options.LowercaseUrls = true); +builder.Services.AddDbContext(opt => +{ + opt.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); + opt.LogTo(message => Debug.WriteLine(message)); +}); builder.Services.AddScoped(typeof(IRepository), typeof(Repo)); builder.Services.AddScoped(typeof(IRepository), typeof(Repo)); builder.Services.AddScoped(typeof(IRepository), typeof(Repo)); builder.Services.AddScoped(typeof(IRepository), typeof(Repo)); - +builder.Services.AddScoped(); +builder.Services.AddControllers().AddJsonOptions(opt => +{ + opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); +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(); +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. @@ -30,5 +110,12 @@ } app.UseHttpsRedirection(); +app.UseStatusCodePages(); + +app.UseAuthentication(); +app.UseAuthorization(); + app.ConfigureCinemaEndpoint(); + +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..f737b586 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Http.HttpResults; + +namespace api_cinema_challenge.Services; + +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Tokens; +using System.Security.Claims; +using System.Text; +using api_cinema_challenge.Models; +using Microsoft.AspNetCore.Identity; + +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 + ); + } +} \ No newline at end of file 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 ebb36b31..30637589 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 @@ -8,13 +8,9 @@ - - - - - - - + + +