From 09528ab3fa99cd24d653254c7de500c5f0935b3d Mon Sep 17 00:00:00 2001 From: Chris Sivert Sylte Date: Mon, 25 Aug 2025 11:57:41 +0200 Subject: [PATCH 1/6] added customers, movies and screenings from boolean --- .../Data/CinemaContext.cs | 15 +- .../Endpoints/CustomerEndpoints.cs | 42 ++++++ .../Endpoints/MovieEndpoints.cs | 42 ++++++ .../Endpoints/ScreeningEndpoints.cs | 21 +++ .../20250825083406_movieTable.Designer.cs | 99 +++++++++++++ .../Migrations/20250825083406_movieTable.cs | 61 ++++++++ ...84022_ChangeMovieTitleToString.Designer.cs | 100 +++++++++++++ ...20250825084022_ChangeMovieTitleToString.cs | 34 +++++ ...20250825085423_screeningsTable.Designer.cs | 126 ++++++++++++++++ .../20250825085423_screeningsTable.cs | 39 +++++ ...0250825085705_screeningsTable2.Designer.cs | 119 +++++++++++++++ .../20250825085705_screeningsTable2.cs | 60 ++++++++ ...0250825094651_screeningsTable3.Designer.cs | 138 ++++++++++++++++++ .../20250825094651_screeningsTable3.cs | 60 ++++++++ .../Migrations/CinemaContextModelSnapshot.cs | 135 +++++++++++++++++ .../api-cinema-challenge/Models/Customer.cs | 9 ++ .../api-cinema-challenge/Models/Movie.cs | 10 ++ .../api-cinema-challenge/Models/Screening.cs | 9 ++ .../api-cinema-challenge/Program.cs | 17 +++ .../Repository/CustomerRepository.cs | 54 +++++++ .../Repository/ICustomerRepository.cs | 8 + .../Repository/IMovieRepository.cs | 8 + .../Repository/IScreeningRepository.cs | 5 + .../Repository/MovieRepository.cs | 54 +++++++ .../Repository/ScreeningRepository.cs | 28 ++++ .../appsettings.example.json | 12 -- 26 files changed, 1292 insertions(+), 13 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825083406_movieTable.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825083406_movieTable.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825084022_ChangeMovieTitleToString.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825084022_ChangeMovieTitleToString.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825085423_screeningsTable.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825085423_screeningsTable.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825085705_screeningsTable2.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825085705_screeningsTable2.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825094651_screeningsTable3.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825094651_screeningsTable3.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/Repository/CustomerRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/ICustomerRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/IMovieRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/IScreeningRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/ScreeningRepository.cs delete mode 100644 api-cinema-challenge/api-cinema-challenge/appsettings.example.json diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index ad4fe854..9c84054b 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -6,11 +6,18 @@ namespace api_cinema_challenge.Data public class CinemaContext : DbContext { private string _connectionString; + public DbSet Customers { get; set; } = null!; + public DbSet Movies { get; set; } = null!; + public DbSet Screenings { get; set; } = null!; + + 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) @@ -20,7 +27,13 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity() + .Property(c => c.CreatedAt) + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + modelBuilder.Entity() + .Property(c => c.UpdatedAt) + .HasDefaultValueSql("CURRENT_TIMESTAMP"); } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs new file mode 100644 index 00000000..5e4385ca --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; + +public static class CustomerEndpoints +{ + public static void MapCustomerEndpoints(this WebApplication app) + { + app.MapGet("/customers", async ([FromServices] ICustomerRepository repo) => + { + var customers = await repo.GetAllAsync(); + return Results.Ok(customers); + }) + .WithTags("Customers"); + + app.MapGet("/customers/{id}", async (int id, [FromServices] ICustomerRepository repo) => + { + var customer = await repo.GetByIdAsync(id); + return customer is not null ? Results.Ok(customer) : Results.NotFound(); + }) + .WithTags("Customers"); + + app.MapPost("/customers", async (Customer customer, [FromServices] ICustomerRepository repo) => + { + var createdCustomer = await repo.CreateAsync(customer); + return Results.Created($"/customers/{createdCustomer.Id}", createdCustomer); + }) + .WithTags("Customers"); + + app.MapPut("/customers/{id}", async (int id, Customer customer, [FromServices] ICustomerRepository repo) => + { + var updatedCustomer = await repo.UpdateAsync(id, customer); + return updatedCustomer is not null ? Results.Ok(updatedCustomer) : Results.NotFound(); + }) + .WithTags("Customers"); + + app.MapDelete("/customers/{id}", async (int id, [FromServices] ICustomerRepository repo) => + { + var deleted = await repo.DeleteAsync(id); + return deleted ? Results.Ok() : Results.NotFound(); + }) + .WithTags("Customers"); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs new file mode 100644 index 00000000..bf2b3332 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; + +public static class MovieEndpoints +{ + public static void MapMovieEndpoints(this WebApplication app) + { + app.MapGet("/movies", async ([FromServices] IMovieRepository repo) => + { + var movies = await repo.GetAllAsync(); + return Results.Ok(movies); + }) + .WithTags("Movies"); + + app.MapGet("/movies/{id}", async (int id, [FromServices] IMovieRepository repo) => + { + var movie = await repo.GetByIdAsync(id); + return movie is not null ? Results.Ok(movie) : Results.NotFound(); + }) + .WithTags("Movies"); + + app.MapPost("/movies", async (Movie movie, [FromServices] IMovieRepository repo) => + { + var createdMovie = await repo.CreateAsync(movie); + return Results.Created($"/movies/{createdMovie.Id}", createdMovie); + }) + .WithTags("Movies"); + + app.MapPut("/movies/{id}", async (int id, Movie movie, [FromServices] IMovieRepository repo) => + { + var updatedMovie = await repo.UpdateAsync(id, movie); + return updatedMovie is not null ? Results.Ok(updatedMovie) : Results.NotFound(); + }) + .WithTags("Movies"); + + app.MapDelete("/movies/{id}", async (int id, [FromServices] IMovieRepository repo) => + { + var deleted = await repo.DeleteAsync(id); + return deleted ? Results.Ok() : Results.NotFound(); + }) + .WithTags("Movies"); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs new file mode 100644 index 00000000..7a7919b3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; + +public static class ScreeningEndpoints +{ + public static void MapScreeningEndpoints(this WebApplication app) + { + app.MapGet("/screenings", async ([FromServices] IScreeningRepository repo) => + { + var screening = await repo.GetAllAsync(); + return Results.Ok(screening); + }) + .WithTags("Screenings"); + + app.MapPost("/screenings", async (Screening screening, [FromServices] IScreeningRepository repo) => + { + var createdMovie = await repo.CreateAsync(screening); + return Results.Created($"/screenings/{createdMovie.Id}", createdMovie); + }) + .WithTags("Screenings"); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825083406_movieTable.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825083406_movieTable.Designer.cs new file mode 100644 index 00000000..124f75ef --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825083406_movieTable.Designer.cs @@ -0,0 +1,99 @@ +// +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("20250825083406_movieTable")] + partial class movieTable + { + /// + 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("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825083406_movieTable.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825083406_movieTable.cs new file mode 100644 index 00000000..cb7480cb --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825083406_movieTable.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class movieTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + 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: "integer", nullable: false), + Rating = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + RuntimeMins = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Movies", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Movies"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825084022_ChangeMovieTitleToString.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825084022_ChangeMovieTitleToString.Designer.cs new file mode 100644 index 00000000..4e8f11bf --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825084022_ChangeMovieTitleToString.Designer.cs @@ -0,0 +1,100 @@ +// +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("20250825084022_ChangeMovieTitleToString")] + partial class ChangeMovieTitleToString + { + /// + 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("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825084022_ChangeMovieTitleToString.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825084022_ChangeMovieTitleToString.cs new file mode 100644 index 00000000..d8eaa3cc --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825084022_ChangeMovieTitleToString.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class ChangeMovieTitleToString : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Title", + table: "Movies", + type: "text", + nullable: false, + oldClrType: typeof(int), + oldType: "integer"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Title", + table: "Movies", + type: "integer", + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085423_screeningsTable.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085423_screeningsTable.Designer.cs new file mode 100644 index 00000000..1d94c09f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085423_screeningsTable.Designer.cs @@ -0,0 +1,126 @@ +// +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("20250825085423_screeningsTable")] + partial class screeningsTable + { + /// + 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("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("ScreeningTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Theater") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Screenings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085423_screeningsTable.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085423_screeningsTable.cs new file mode 100644 index 00000000..3e159e8e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085423_screeningsTable.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class screeningsTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Screenings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MovieId = table.Column(type: "integer", nullable: false), + ScreeningTime = table.Column(type: "timestamp with time zone", nullable: false), + Theater = table.Column(type: "text", nullable: false), + Price = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Screenings", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Screenings"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085705_screeningsTable2.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085705_screeningsTable2.Designer.cs new file mode 100644 index 00000000..d6e9fdf2 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085705_screeningsTable2.Designer.cs @@ -0,0 +1,119 @@ +// +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("20250825085705_screeningsTable2")] + partial class screeningsTable2 + { + /// + 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("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Screenings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085705_screeningsTable2.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085705_screeningsTable2.cs new file mode 100644 index 00000000..ebb8b56b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825085705_screeningsTable2.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class screeningsTable2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Price", + table: "Screenings"); + + migrationBuilder.DropColumn( + name: "Theater", + table: "Screenings"); + + migrationBuilder.RenameColumn( + name: "ScreeningTime", + table: "Screenings", + newName: "StartsAt"); + + migrationBuilder.RenameColumn( + name: "MovieId", + table: "Screenings", + newName: "ScreenNumber"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "StartsAt", + table: "Screenings", + newName: "ScreeningTime"); + + migrationBuilder.RenameColumn( + name: "ScreenNumber", + table: "Screenings", + newName: "MovieId"); + + migrationBuilder.AddColumn( + name: "Price", + table: "Screenings", + type: "numeric", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "Theater", + table: "Screenings", + type: "text", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825094651_screeningsTable3.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825094651_screeningsTable3.Designer.cs new file mode 100644 index 00000000..2d4d6b86 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825094651_screeningsTable3.Designer.cs @@ -0,0 +1,138 @@ +// +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("20250825094651_screeningsTable3")] + partial class screeningsTable3 + { + /// + 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("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("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"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("Screening", b => + { + b.HasOne("Movie", "Movie") + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825094651_screeningsTable3.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825094651_screeningsTable3.cs new file mode 100644 index 00000000..134f05d4 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825094651_screeningsTable3.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class screeningsTable3 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Capacity", + table: "Screenings", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "MovieId", + table: "Screenings", + type: "integer", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Screenings_MovieId", + table: "Screenings", + column: "MovieId"); + + migrationBuilder.AddForeignKey( + name: "FK_Screenings_Movies_MovieId", + table: "Screenings", + column: "MovieId", + principalTable: "Movies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Screenings_Movies_MovieId", + table: "Screenings"); + + migrationBuilder.DropIndex( + name: "IX_Screenings_MovieId", + table: "Screenings"); + + migrationBuilder.DropColumn( + name: "Capacity", + table: "Screenings"); + + migrationBuilder.DropColumn( + name: "MovieId", + table: "Screenings"); + } + } +} 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..abb8d3f3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,135 @@ +// +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("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("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"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("Screening", b => + { + b.HasOne("Movie", "Movie") + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); +#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..0802bdea --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,9 @@ +public class Customer +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} 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..f714196e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,10 @@ +public class Movie +{ + public int Id { get; set; } + public string Title { get; set; } = string.Empty; + public string Rating { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string RuntimeMins { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} 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..6813d101 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,9 @@ +public class Screening +{ + public int Id { get; set; } + public int MovieId { get; set; } + public Movie Movie { get; set; } = null!; // Navigation property! + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..700c1758 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,12 +1,22 @@ using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString"))); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + + // Add services to the container. builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -16,5 +26,12 @@ app.UseSwaggerUI(); } +//Endpoints into swagger +app.MapCustomerEndpoints(); +app.MapMovieEndpoints(); +app.MapScreeningEndpoints(); + + + app.UseHttpsRedirection(); app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs new file mode 100644 index 00000000..462203f1 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs @@ -0,0 +1,54 @@ +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; + + +public class CustomerRepository : ICustomerRepository +{ + private readonly CinemaContext _context; + + public CustomerRepository(CinemaContext context) + { + _context = context; + } + + public async Task> GetAllAsync() + { + return await _context.Customers.ToListAsync(); + } + + public async Task GetByIdAsync(int id) + { + return await _context.Customers.FindAsync(id); + } + + public async Task CreateAsync(Customer customer) + { + _context.Customers.Add(customer); + await _context.SaveChangesAsync(); + return customer; + } + + public async Task UpdateAsync(int id, Customer customer) + { + var existingCustomer = await _context.Customers.FindAsync(id); + if (existingCustomer == null) return null; + + existingCustomer.Name = customer.Name; + existingCustomer.Email = customer.Email; + existingCustomer.Phone = customer.Phone; + existingCustomer.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return existingCustomer; + } + + public async Task DeleteAsync(int id) + { + var customer = await _context.Customers.FindAsync(id); + if (customer == null) return false; + + _context.Customers.Remove(customer); + await _context.SaveChangesAsync(); + return true; + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/ICustomerRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/ICustomerRepository.cs new file mode 100644 index 00000000..035d528d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/ICustomerRepository.cs @@ -0,0 +1,8 @@ +public interface ICustomerRepository +{ + Task> GetAllAsync(); + Task GetByIdAsync(int id); + Task CreateAsync(Customer customer); + Task UpdateAsync(int id, Customer customer); + Task DeleteAsync(int id); +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/IMovieRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/IMovieRepository.cs new file mode 100644 index 00000000..3128585f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IMovieRepository.cs @@ -0,0 +1,8 @@ +public interface IMovieRepository +{ + Task> GetAllAsync(); + Task GetByIdAsync(int id); + Task CreateAsync(Movie customer); + Task UpdateAsync(int id, Movie customer); + Task DeleteAsync(int id); +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/IScreeningRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/IScreeningRepository.cs new file mode 100644 index 00000000..5b32e6fc --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IScreeningRepository.cs @@ -0,0 +1,5 @@ +public interface IScreeningRepository +{ + Task> GetAllAsync(); + Task CreateAsync(Screening screening); +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs new file mode 100644 index 00000000..45384d40 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs @@ -0,0 +1,54 @@ +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; + + +public class MovieRepository : IMovieRepository +{ + private readonly CinemaContext _context; + + public MovieRepository(CinemaContext context) + { + _context = context; + } + + public async Task> GetAllAsync() + { + return await _context.Movies.ToListAsync(); + } + + public async Task GetByIdAsync(int id) + { + return await _context.Movies.FindAsync(id); + } + + public async Task CreateAsync(Movie movie) + { + _context.Movies.Add(movie); + await _context.SaveChangesAsync(); + return movie; + } + + public async Task UpdateAsync(int id, Movie movie) + { + var existingCustomer = await _context.Movies.FindAsync(id); + if (existingCustomer == null) return null; + + existingCustomer.Title = movie.Title; + existingCustomer.Rating = movie.Rating; + existingCustomer.RuntimeMins = movie.RuntimeMins; + existingCustomer.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return existingCustomer; + } + + public async Task DeleteAsync(int id) + { + var customer = await _context.Customers.FindAsync(id); + if (customer == null) return false; + + _context.Customers.Remove(customer); + await _context.SaveChangesAsync(); + return true; + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/ScreeningRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/ScreeningRepository.cs new file mode 100644 index 00000000..71674cc4 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/ScreeningRepository.cs @@ -0,0 +1,28 @@ +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; + + +public class ScreeningRepository : IScreeningRepository +{ + private readonly CinemaContext _context; + + public ScreeningRepository(CinemaContext context) + { + _context = context; + } + + + public async Task> GetAllAsync() + { + return await _context.Screenings + .Include(s => s.Movie) + .ToListAsync(); + } + + public async Task CreateAsync(Screening screening) + { + _context.Screenings.Add(screening); + await _context.SaveChangesAsync(); + return screening; + } +} 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 67066fa70a338b32b551cf1ff6ffc11f7dae3f76 Mon Sep 17 00:00:00 2001 From: Chris Sivert Sylte Date: Mon, 25 Aug 2025 13:11:15 +0200 Subject: [PATCH 2/6] added simple (but not safe) login --- .../Data/CinemaContext.cs | 1 + .../Endpoints/CustomerEndpoints.cs | 11 +- .../Endpoints/LoginEndpoints.cs | 47 ++++++ .../Endpoints/MovieEndpoints.cs | 11 +- .../Endpoints/RegisterEndpoints.cs | 18 ++ .../Endpoints/ScreeningEndpoints.cs | 5 +- .../20250825105802_userTable.Designer.cs | 159 ++++++++++++++++++ .../Migrations/20250825105802_userTable.cs | 36 ++++ .../Migrations/CinemaContextModelSnapshot.cs | 21 +++ .../Models/LoginRequest.cs | 5 + .../api-cinema-challenge/Models/User.cs | 6 + .../api-cinema-challenge/Program.cs | 67 +++++++- .../api-cinema-challenge.csproj | 2 + 13 files changed, 371 insertions(+), 18 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/LoginEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/RegisterEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825105802_userTable.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825105802_userTable.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/LoginRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/User.cs diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index 9c84054b..e38cc08a 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -9,6 +9,7 @@ public class CinemaContext : DbContext public DbSet Customers { get; set; } = null!; public DbSet Movies { get; set; } = null!; public DbSet Screenings { get; set; } = null!; + public DbSet Users { get; set; } = null!; public CinemaContext(DbContextOptions options) : base(options) diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs index 5e4385ca..9610482f 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs @@ -1,38 +1,39 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; public static class CustomerEndpoints { public static void MapCustomerEndpoints(this WebApplication app) { - app.MapGet("/customers", async ([FromServices] ICustomerRepository repo) => + app.MapGet("/customers", [Authorize] async ([FromServices] ICustomerRepository repo) => { var customers = await repo.GetAllAsync(); return Results.Ok(customers); }) .WithTags("Customers"); - app.MapGet("/customers/{id}", async (int id, [FromServices] ICustomerRepository repo) => + app.MapGet("/customers/{id}", [Authorize] async (int id, [FromServices] ICustomerRepository repo) => { var customer = await repo.GetByIdAsync(id); return customer is not null ? Results.Ok(customer) : Results.NotFound(); }) .WithTags("Customers"); - app.MapPost("/customers", async (Customer customer, [FromServices] ICustomerRepository repo) => + app.MapPost("/customers", [Authorize] async (Customer customer, [FromServices] ICustomerRepository repo) => { var createdCustomer = await repo.CreateAsync(customer); return Results.Created($"/customers/{createdCustomer.Id}", createdCustomer); }) .WithTags("Customers"); - app.MapPut("/customers/{id}", async (int id, Customer customer, [FromServices] ICustomerRepository repo) => + app.MapPut("/customers/{id}", [Authorize] async (int id, Customer customer, [FromServices] ICustomerRepository repo) => { var updatedCustomer = await repo.UpdateAsync(id, customer); return updatedCustomer is not null ? Results.Ok(updatedCustomer) : Results.NotFound(); }) .WithTags("Customers"); - app.MapDelete("/customers/{id}", async (int id, [FromServices] ICustomerRepository repo) => + app.MapDelete("/customers/{id}", [Authorize] async (int id, [FromServices] ICustomerRepository repo) => { var deleted = await repo.DeleteAsync(id); return deleted ? Results.Ok() : Results.NotFound(); diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/LoginEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/LoginEndpoints.cs new file mode 100644 index 00000000..afabd72f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/LoginEndpoints.cs @@ -0,0 +1,47 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using api_cinema_challenge.Data; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; + +public static class LoginEndpoints +{ + public static void MapLoginEndpoints(this WebApplication app, IConfiguration config) + { + var jwtSettings = config.GetSection("Jwt"); + + app.MapPost("/login", async (LoginRequest login, CinemaContext db) => + { + var user = await db.Users.SingleOrDefaultAsync(u => u.Username == login.Username); + if (user == null || user.PasswordHash != login.Password) + return Results.Unauthorized(); + + var key = Encoding.UTF8.GetBytes(jwtSettings.GetValue("Key")); + + var tokenHandler = new JwtSecurityTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new System.Security.Claims.ClaimsIdentity(new[] + { + new System.Security.Claims.Claim("id", user.Id.ToString()) + }), + + Expires = DateTime.UtcNow.AddMinutes(jwtSettings.GetValue("ExpiryMinutes")), + Issuer = jwtSettings["Issuer"], + Audience = jwtSettings["Audience"], + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + + + var token = tokenHandler.CreateToken(tokenDescriptor); + var jwt = tokenHandler.WriteToken(token); + + return Results.Ok(new { Token = jwt }); + }) + .WithTags("Authentication"); + + } + +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs index bf2b3332..a4646fbe 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs @@ -1,38 +1,39 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; public static class MovieEndpoints { public static void MapMovieEndpoints(this WebApplication app) { - app.MapGet("/movies", async ([FromServices] IMovieRepository repo) => + app.MapGet("/movies", [Authorize] async ([FromServices] IMovieRepository repo) => { var movies = await repo.GetAllAsync(); return Results.Ok(movies); }) .WithTags("Movies"); - app.MapGet("/movies/{id}", async (int id, [FromServices] IMovieRepository repo) => + app.MapGet("/movies/{id}", [Authorize] async (int id, [FromServices] IMovieRepository repo) => { var movie = await repo.GetByIdAsync(id); return movie is not null ? Results.Ok(movie) : Results.NotFound(); }) .WithTags("Movies"); - app.MapPost("/movies", async (Movie movie, [FromServices] IMovieRepository repo) => + app.MapPost("/movies", [Authorize] async (Movie movie, [FromServices] IMovieRepository repo) => { var createdMovie = await repo.CreateAsync(movie); return Results.Created($"/movies/{createdMovie.Id}", createdMovie); }) .WithTags("Movies"); - app.MapPut("/movies/{id}", async (int id, Movie movie, [FromServices] IMovieRepository repo) => + app.MapPut("/movies/{id}", [Authorize] async (int id, Movie movie, [FromServices] IMovieRepository repo) => { var updatedMovie = await repo.UpdateAsync(id, movie); return updatedMovie is not null ? Results.Ok(updatedMovie) : Results.NotFound(); }) .WithTags("Movies"); - app.MapDelete("/movies/{id}", async (int id, [FromServices] IMovieRepository repo) => + app.MapDelete("/movies/{id}", [Authorize] async (int id, [FromServices] IMovieRepository repo) => { var deleted = await repo.DeleteAsync(id); return deleted ? Results.Ok() : Results.NotFound(); diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/RegisterEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/RegisterEndpoints.cs new file mode 100644 index 00000000..c0bfde1c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/RegisterEndpoints.cs @@ -0,0 +1,18 @@ +using api_cinema_challenge.Data; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +public static class RegisterEndpoints +{ + public static void MapRegisterEndpoints(this WebApplication app) + { + app.MapPost("/register", async (User user, CinemaContext db) => + { + //password in plaintext :( + db.Users.Add(user); + await db.SaveChangesAsync(); + return Results.Ok(user); + }); + + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs index 7a7919b3..6b1c39b6 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs @@ -1,17 +1,18 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; public static class ScreeningEndpoints { public static void MapScreeningEndpoints(this WebApplication app) { - app.MapGet("/screenings", async ([FromServices] IScreeningRepository repo) => + app.MapGet("/screenings", [Authorize] async ([FromServices] IScreeningRepository repo) => { var screening = await repo.GetAllAsync(); return Results.Ok(screening); }) .WithTags("Screenings"); - app.MapPost("/screenings", async (Screening screening, [FromServices] IScreeningRepository repo) => + app.MapPost("/screenings", [Authorize] async (Screening screening, [FromServices] IScreeningRepository repo) => { var createdMovie = await repo.CreateAsync(screening); return Results.Created($"/screenings/{createdMovie.Id}", createdMovie); diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105802_userTable.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105802_userTable.Designer.cs new file mode 100644 index 00000000..7546b4a5 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105802_userTable.Designer.cs @@ -0,0 +1,159 @@ +// +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("20250825105802_userTable")] + partial class userTable + { + /// + 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("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("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"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Screening", b => + { + b.HasOne("Movie", "Movie") + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105802_userTable.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105802_userTable.cs new file mode 100644 index 00000000..33fed9cc --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825105802_userTable.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class userTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Username = table.Column(type: "text", nullable: false), + PasswordHash = table.Column(type: "text", 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 abb8d3f3..51c65d76 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -119,6 +119,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Screenings"); }); + modelBuilder.Entity("User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + modelBuilder.Entity("Screening", b => { b.HasOne("Movie", "Movie") diff --git a/api-cinema-challenge/api-cinema-challenge/Models/LoginRequest.cs b/api-cinema-challenge/api-cinema-challenge/Models/LoginRequest.cs new file mode 100644 index 00000000..15bf64ce --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/LoginRequest.cs @@ -0,0 +1,5 @@ +public class LoginRequest +{ + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Models/User.cs b/api-cinema-challenge/api-cinema-challenge/Models/User.cs new file mode 100644 index 00000000..422b2287 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/User.cs @@ -0,0 +1,6 @@ +public class User +{ + public int Id { get; set; } + public string Username { get; set; } = string.Empty; + public string PasswordHash { get; set; } = string.Empty; //not safe :( +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 700c1758..e8711154 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,5 +1,8 @@ using api_cinema_challenge.Data; using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; var builder = WebApplication.CreateBuilder(args); @@ -10,28 +13,80 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); - // Add services to the container. builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Name = "Authorization", + Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http, + Scheme = "Bearer", + BearerFormat = "JWT", + In = Microsoft.OpenApi.Models.ParameterLocation.Header, + Description = "Enter 'Bearer' [space] and then your valid token." + }); + + c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement + { + {gi + new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Reference = new Microsoft.OpenApi.Models.OpenApiReference + { + Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); +}); builder.Services.AddDbContext(); +// JWT config +var jwtSettings = builder.Configuration.GetSection("Jwt"); +var key = Encoding.ASCII.GetBytes(jwtSettings.GetValue("Key")); +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) +.AddJwtBearer(options => +{ + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSettings["Issuer"], + ValidAudience = jwtSettings["Audience"], + IssuerSigningKey = new SymmetricSecurityKey(key) + }; +}); + + +builder.Services.AddAuthorization(); var app = builder.Build(); -// Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); + //Endpoints into swagger app.MapCustomerEndpoints(); app.MapMovieEndpoints(); app.MapScreeningEndpoints(); +app.MapRegisterEndpoints(); +app.MapLoginEndpoints(builder.Configuration); - - -app.UseHttpsRedirection(); app.Run(); 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..79b7345e 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -15,6 +15,7 @@ + @@ -25,6 +26,7 @@ + From 4dd6e87fb8663f1455404bface5c18141384238a Mon Sep 17 00:00:00 2001 From: Chris Sivert Sylte Date: Mon, 25 Aug 2025 13:11:48 +0200 Subject: [PATCH 3/6] spelling error --- api-cinema-challenge/api-cinema-challenge/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e8711154..cf6419b8 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -29,7 +29,7 @@ c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement { - {gi + { new Microsoft.OpenApi.Models.OpenApiSecurityScheme { Reference = new Microsoft.OpenApi.Models.OpenApiReference From caa97f5a4fa7fda73aa01e180840228aef0a5499 Mon Sep 17 00:00:00 2001 From: Chris Sivert Sylte Date: Mon, 25 Aug 2025 15:04:26 +0200 Subject: [PATCH 4/6] bookings added --- .../api-cinema-challenge/DTO/BookingDto.cs | 8 + .../DTO/CreateBookingDto.cs | 5 + .../DTO/RegisterCustomerDto.cs | 7 + .../Data/CinemaContext.cs | 22 +- .../api-cinema-challenge/Data/ER diagram.png | Bin 0 -> 59179 bytes .../Endpoints/BookingEndpoints.cs | 50 ++++ .../Endpoints/LoginEndpoints.cs | 6 +- .../Endpoints/RegisterEndpoints.cs | 22 +- .../20250825114838_bookingTable.Designer.cs | 206 +++++++++++++++++ .../Migrations/20250825114838_bookingTable.cs | 52 +++++ ...0250825120320_removedUserTable.Designer.cs | 214 ++++++++++++++++++ .../20250825120320_removedUserTable.cs | 40 ++++ .../20250825120931_smallchange.Designer.cs | 214 ++++++++++++++++++ .../Migrations/20250825120931_smallchange.cs | 28 +++ .../20250825121333_smallchange3.Designer.cs | 214 ++++++++++++++++++ .../Migrations/20250825121333_smallchange3.cs | 28 +++ .../20250825124543_smallchange4.Designer.cs | 214 ++++++++++++++++++ .../Migrations/20250825124543_smallchange4.cs | 102 +++++++++ .../Migrations/CinemaContextModelSnapshot.cs | 57 ++++- .../api-cinema-challenge/Models/Booking.cs | 10 + .../api-cinema-challenge/Models/Customer.cs | 11 +- .../Models/LoginRequest.cs | 2 +- .../api-cinema-challenge/Models/Movie.cs | 1 + .../api-cinema-challenge/Program.cs | 2 + .../Repository/BookingRepository.cs | 28 +++ .../Repository/CustomerRepository.cs | 2 +- .../Repository/IBookingRepository.cs | 5 + 27 files changed, 1531 insertions(+), 19 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/DTO/BookingDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTO/CreateBookingDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTO/RegisterCustomerDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Data/ER diagram.png create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/BookingEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825114838_bookingTable.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825114838_bookingTable.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825120320_removedUserTable.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825120320_removedUserTable.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825120931_smallchange.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825120931_smallchange.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825121333_smallchange3.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825121333_smallchange3.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825124543_smallchange4.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825124543_smallchange4.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Booking.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/BookingRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/IBookingRepository.cs diff --git a/api-cinema-challenge/api-cinema-challenge/DTO/BookingDto.cs b/api-cinema-challenge/api-cinema-challenge/DTO/BookingDto.cs new file mode 100644 index 00000000..2ccf762a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTO/BookingDto.cs @@ -0,0 +1,8 @@ +public class BookingDto +{ + public int CustomerId { get; set; } + public int MovieId { get; set; } + public DateTime BookedAt { get; set; } + public string? CustomerName { get; set; } + public string? MovieTitle { get; set; } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTO/CreateBookingDto.cs b/api-cinema-challenge/api-cinema-challenge/DTO/CreateBookingDto.cs new file mode 100644 index 00000000..ec19a75e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTO/CreateBookingDto.cs @@ -0,0 +1,5 @@ +public class CreateBookingDto +{ + public int CustomerId { get; set; } + public int MovieId { get; set; } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTO/RegisterCustomerDto.cs b/api-cinema-challenge/api-cinema-challenge/DTO/RegisterCustomerDto.cs new file mode 100644 index 00000000..dd4defbb --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTO/RegisterCustomerDto.cs @@ -0,0 +1,7 @@ +public class RegisterCustomerDto +{ + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Phonenumber { get; set; } = string.Empty; +} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index e38cc08a..c43fbbfe 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -10,15 +10,13 @@ public class CinemaContext : DbContext public DbSet Movies { get; set; } = null!; public DbSet Screenings { get; set; } = null!; public DbSet Users { get; set; } = null!; + public DbSet Bookings { get; set; } = null!; public CinemaContext(DbContextOptions options) : base(options) { var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); _connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString")!; - // this.Database.EnsureCreated(); - - } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) @@ -28,6 +26,9 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity() + .HasKey(b => new { b.CustomerId, b.MovieId }); + modelBuilder.Entity() .Property(c => c.CreatedAt) .HasDefaultValueSql("CURRENT_TIMESTAMP"); @@ -35,6 +36,19 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .Property(c => c.UpdatedAt) .HasDefaultValueSql("CURRENT_TIMESTAMP"); - } + + modelBuilder.Entity() + .HasKey(b => new { b.CustomerId, b.MovieId }); + + modelBuilder.Entity() + .HasOne(b => b.Customer) + .WithMany(c => c.Bookings) + .HasForeignKey(b => b.CustomerId); + + modelBuilder.Entity() + .HasOne(b => b.Movie) + .WithMany(m => m.Bookings) + .HasForeignKey(b => b.MovieId); + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Data/ER diagram.png b/api-cinema-challenge/api-cinema-challenge/Data/ER diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..dacfe4204afa671a979a5f3867e648e06725d5b1 GIT binary patch literal 59179 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%0Dr$c}R~fpg#vA$0u|uj57;Eg(nH8a`$_ z+%u4D^U~%$VsHtr&2@B4?Lj@-S~Z3C65xNlTlywGMn}igN|B$Rzl6|t49X(lXNvnd z^Q77B`?*>n=7-&PSD_Z5r&=`T;Ezfg!U*l=JM>msM;%{vF`pH0u%;WSCvHyyzZ?jQ zr+zR>XpV+o31@exbZYwn4su)5bxy4+E-E={8pAqNErLGl@977%SgVGLdWSQ#W1sdCX8q#=&E4l`o_Uv(# zl+c;ijNpYzR&B5QG@4pHDE6pdVzR6O3e+#Yr(sO8crq&bdHO~9*#VmyMzGsV9rphA>}7dkCWYI?33N_2i#x=wH6uRm ze93|zvf6z=YwaYA_IfNU&hiHLVx>}t`_a4AttCRk$4iRZ`abQz5*vL|o(vcfNS^Jx zcI(&RCU?t^&$0bV;N03*Gnu@n1i0Ml4@{ydPl`ViGi4ksp*|abLlg-2K-*clZKE?e%_agKmk68hn91Dc-Xbe| z(od89Ss6Bq)pHT%CCOop=?i8ch8@+ziD*KH)$ZI@-~A4;MfQoguH~X_^>bWz!^+p3 z;f+Re9f?@n@rr}h)wARcy?YLTDni~;x!Tflk!tSl*#%tl9Ic*!dk1S4*ye=@05_#D znZ3Nctgf>bV-L92xpLt;Bp5`F7aC1lqcnN^Fh~o7qwm!^N}U>{Hz)9UIfcxTNEO%D zH@!|E{Gy zj7B_U!7Vnckm56>rfJ;Gj$PCI;2p_}N-#6G4zT0F)Fe$_kJny+@RhEY5~Qevn@!aHX^PJ-*C~r!nlN0tiF@ z@p>uJj$n<5SpSPOBnWsiA0i^^ z)MM>MjnPw>+}NVbWj2a>eNOjsFPCkRYL~5p+YKXutMv*4tBRi@`q1mr%nejqcPzo*^lgpc+sOt#4`L!gVSS-L`i(x`VdCnnUB#%|1(>>2Ze4wNq zYN}#4tH|muc+mjKs>-;2fpwYcyTr1CR(=t!)q#k9cjElR52d52C6y+1pQA{g+$qMK zkFy&NXi!ND6t>{gLZP8J_0|K7?1pH^Z7TU{gr20my{hK1HF1eDT1mflh0}Mx{?8oe za$sKMcmCH!X|eNUm^}!?=BirnpJa2(%feSShzu#3QsY0$75Uxc9)9dP*=Bhzx$!xB zB*ESJg-dqX+Tgw?^{}}jy#Kz z_m6wv6xH1*sW)G1yWsaeH=TCydwiAcbIeSPe2`e2w)6HZSB~uTAsBhT_N5NVZYs?c z+#wD_NIkZE`<{+6uIFOtw&o=LLZ&!jg#$wP+Gb6GHQ&iDbliz`%{sr8llG~LRL=T;GH zc#8>G$niH+s^fc9P&& zF3g@5xONC&eZnVY8z_in%*os8JuD~Rway7o_19etI*eOVaA+p=9u&6Q6nM#~*cPo- znnRc~e!Y!fOE%_AZjrOZ^jbh=E6eq98aiN>7iicWdXM8as)&ThrSDqocNV#`#8NC@ zGzBFn>OLwQI2B}OlYK?A7C#2hF8#?m@ELseIbT1sb~Dx1#KAbAokx1#N7s#&WS^*)S#0cW$j5 zvLWX2^uYwMw`*tpS!bMrtRl0wsM24MJJ9!{agwrnS-CA=Mzxqz;zIj&UA8 zw;4{JC~d1&$hIvpxnedyu3XV9heqIu4zqzv11o$PkuEyt*dAC#AMk}_1=`kWrm$OD zaPz|Wq5T&`kNnN~h#*U*XYIQt50}0O=)b#}hx6k>&0LW0i2e5d@D4?8 zvn#wUbZ6)1&XUjgN7K35&>&a#>qqFCqXqAc|3%tchsE{uTfVpiO>k{O0t9!rkl-2! z?oM!bcXxtIa0nJ4xCM82cXw^1fxF4?yz`!OX68OK_x3;i&_MUz)q7X1THnvAV(qc) zCI1f3!3($CY{ys5H6!v&g`BhJim_PhFyZM0>k^IQG_=yDz)#G{6Bdn$SB53Ta&RYj9idr}@^Ir%`=(Rt?<3b?@x-`}iKujmT~O zw9hl2|5S3KW-qN(0o?=fLNe$M<~vODiufSD@j0N+G|{k_B7%^bu@ELvhbAK&Rc0dC zSl5g-EK!~?a=jg!MCg_ift@dbwrK3q}gBEH`^_4(rDsQ?l&PR`f|B zJMj)9rFEtbq&)lG6ZjO*IO+1HDlC!8!jZxyB+(rL+yjRXp`c#dO z^#sPR{cY0)V-_Cx2{Un8hWM^6Fz@=dm}r{L3+TH_Okje>Q33=J1WckFynC5bq(3G{ zNAizvb}9Uh+Hn7he6;#;d?UT@wyoxx%}LV?vPR%1q`R^NmreD(6p{Xq=?Ffhyqp9Z zE)&IfYFIDQGpzQnR8Yy|_Jne2CTp9sx(G1sleX@LXWgT{k6a1h6$&WT6B6PamJ<@l z&mO~6k|s1i%gXZm^`^biW^ck@G2aQ=eyb<~_kz#bg9*7{p~>C- z4IS}@c~>!%*Prxz<_dyUsb_)$sCMYn{C51+>b_H3vI}0;fWb$tx>76s*#T6AUGDj2 zJ~Y{xXx*8XkdVGs!Dpj8W|uN*iJUZRIqcsR62 zhMovKi_RPBH34FoX2fjJW_b)(s30S;?f5=#9%o)#WkS-lcjk^aH90Ubl4dz$Y1qYc zc=OqV!?aYs44@STR`iFlc1v-7ya)=_r!HRt5dCrX)69WLx5pvFW}H=+ zS|aknFYtZ_PfNipMhp25D~zYk%R=)5eP^$IN((u^CL?5fn$I)o;`;-F=|biY6A%2P z<(~?F;l<{Awd1+3f7+gnAfFw%Gr<$LlVsUnzcMY8s5~`ABMy^e1V>Up6$3p1wbw%T zi2tzzv6X(turRn1K|2a`$n^(xXeYY|+G$ge6v~*zAOxXtoM|)@*2lmek%Z(k-%PX~ z&Ts^`s?P`~Vw>Muv6S2vL9wbz3wT$^?>P7jV_XD-Y4aq-Cu?LA87AQwF2m>Ur2gje zp-p7{acti!aI6wH)lXOF_YcMdG#1{AR90n9G%pN;=$39&?!a#mr}HUb|Bs?u&c+{- zE}N_p;sP)4-V6nNyBl|F4)r<)G+3M=aAmTwoUjYsq6&2meJhwR{!sUER(1x`_j&GC ztdT0f{n2enuTT5({X8{r2F9@M=qtBmR*P03dc=*i_)ot}hdXOat=0%*CRvhM2jQgk z%q&n3hoP1oz?LTKx_@Z2-;?dc6R-w(agUt?J5g>+-KK#PvM;K^fIhF z=XrvD$7ae{@xZ0&bYQi)M`tC0G0-&(aF2h2n+}VeAA*l^5XB!GVclQ3Jk8vyce>oL zy?&4O&~e)`>~ZOB*k0+xam+0|rzq}g+4teqPMN?#PT11wGZyCFoUj~Bapa0+qA}>4OJr=OU%=}O~_g;ru1MKQu zAL@Z4Bj`_!K0~v*m{LBL$|bV#4)?sPO`%@x%{DWAY4)86RaDbzzEL%nNq%@xE|YU$ zI4mQfNoI6MslF3?(+4)hZ>5eKOG_Ooh%|F|rJeQLMiMnUacG@%4O5H;-LmRB4jc6k z!dvM-56^Cf^6PJ)1Z93Z8Cwj?4`I_0w9~+h&uT(2UFuZUOG|UgkC*#)Yb%V`=l?SOT%o?J;M{x>O zjj7-EnP&QgF$ExR=&7swzF?9x>{(h>XQ>Lj^#|sa;w;j0Oaoehiga@Mw(jqOwz())nYdni^>aU$tRW!v8%W=C zN650^zD#ZV(BAj|Gtsj$e3c7)40#;e|L$3$mE;@xWZ}1Y%Gl6Pb+8%uakji;;dqP} zz*csdasqc@_1lRW$t+!EgYn014J1XtT z(N#~d7SQzkR+$_b8rE$YQ)!g37r-=HHnl5nMca+al>S@8x%9ztK4;^b`$Eoqo65Q* z`(3Zzz-WPu@}`pw;469)PubUo7C1hLiIQ8lVz%MjhC);#@?OG9PRc-1G7OlBYeg~< zqU01bJrhk@%j!)OW)X6}j^*-BPUx6_eii4`OX6`isKcR571*(76cRYzfFU^aThVbc zTpF7S7Ft2zQBjYrzuW=;@z~mXkjT2(cmL$Mr;Jl30+%$_yge`>?QXcDu^znQE+Hq! zsjTDpDmN+HduR`s@e3ishcAV&89V6LPv~6@bFRMkSch?5@@i5S77^Xw499D@wM|Y> zuOKJ^HYTTtFf*lk9ZS?#nB^t;0ed4+L*`Wuxq;2#RI!7z&f3v@bq@`}yFmGuKe@Zy zg-*7!537_E7G2ws6EvF0OS^xf@81S2h1U(fS65e1O#7210-vO|8kvgglG3bX_aExs zC&>X<##T@s6Ag=U>J4Y>{|8e+SQu3R5^O?ELqm8w=QLDLfLe2mfqs!~+gYOfrvcTr zl^kmr4kLFNZt=jje5jLSH4NA$Db*lX*O4T%+-kFjMI5jLluKy|kqAo=&J~Pm^viAt zX@jsAbO@b~#`$K&18gg56n@j{mg0Z9f&_bFkB{LjH@dBj!a8EjDPhm=hPv@|_PD)n zcuV5xuY*5$8&bEzWkiP^E1z7i$99x!b@z_0cqz2PgaO`Ms~(u0W%UK+qH!hhf9hX} zMfhI1e{!Ml?7;sn+JrOaSDH)>)d!D8{OU@psYYbd;zAE|2(M*FWdTgBa-}`ntZO{{ zLsdSV%&kj8glP{+L?iN%xg)*A}1OAiJ)i4?tD=e80q5 z?BYan_(njoPlok`>5Hy2U5N77?~;HB#Z6q`xVnMM>ux74vOCTJba7K0jez^O!DWp~ zC-IZ_+2Ni0%~2?yK@l(d&UeNy22x{m-&2JojF1yYmwNW=Dl86* z-k-G(9&zFKJRMuxhFUnMDD*!%LGMJlIcc^tk}&%&i(ogeQ8{YE%QS`ci!)ojwR4nC zWeZ)jZd+ERlVA;3(+PpBYGhd3GMDd6RpzF{*4vX_o`jZQ#=cJT(fds#SAzKKOMRDq z=~0qePi8G;?Uso!N)YKKJ^@G-XLfl-=GU-DzrKb8ogRAawk%;(Hv*z)k6vGcfzxLBKluX+YvD9x4K*}c;}gGrboNO+$k}7I z`&Cxj$@$GfKY3(_MiO^hc*MRn(ys>1Av1E7l1I6E;j^x_>6QyO$4_-cl2RRKn=x+o zX4jtj=@^Lj>PM3>5;0ki>TKJgZ_lH?&gw<#rthf6&G*cH^r=S?t?9aM@$(Wp&kc#$ zCVysodR!ZI=FSUCb0#USM!P zi8`cC_+y;?$uB3u6Wp2;wAikA+RXyXu-soh->FQoKio12tixCzAFLMSaJQ@GM|<_? zfMt^J&Ao6ff3-}(71%#{8y;djr=N<}-JQ@-c`u)_%A#FTecqA;ntCeulu37lf_GV! zQ{dCXpqj$GxQi3?2;j3_8*SaV6uHv~E*uN`FWKRBUqV%VD0f`HzafU|2=4Cm!0n__fYBK<)iAxS zMKB)4n&V3&M=LK+L>*67oH{4f#F1jDvwSrVUbABYT}6j^vrThrsFHV@WIG06rX*FZ zWy8@AK=DHpW~%#D-@&osDt-B?(HWlteUP<)i5!r$wfo&)$mK&)LlUt!GUT2@xoj># z@KNHYxp`4vTKoPDkK{m3HamS%*n)l))DF((PsekYt+J>pCTD>$HzaG1Igl7&kYKm0 zA=)!9;s{gpKOL_=Mea`)+V_$;B=^jtPouu0jCSn6Eb0}Wn~#RIK?s*) ziXZX9slWT6P0JV2b#mUDZot)07qgP&tRT68E;byhi?>8zif`$Q;%7xWyPUYB4=CWa zsrknl0X&!5x{2QlpZ59?4i*qbE->=xO-5G;!|)bu`Q$$jV;Lj4?n_r>ySl>%h96-o zbI8XdI^4Rv!>#A3m3L$cpdJ-DY<+r6yHVEVJ}mIa4Tc`YKG+$L)cBxbQFylhirBeZ z<>t_8Rdm_*SLyuc$$Nd`aANEVYyHs!J@xF(K-n{6%pr^s0yR9(`n02k8R*d(id8r& zjP-q(0;8Cg54u$rVkxRuAB zV3WCo^axSwY#r>Kob0SVKd#-d7@^PP-2$Tl-^pfAty{6}{Z{DQaDu1n}P=XgiO^8=NtvNaIq@RO*g=Lbv3 zrO)B2ShLsr#E`e1Ed`SSTxRQ9^& zK1`DzizPx^jg=|Q*ZD@iZdr&e1J=AMlJ|=D>D37X)2?9Da!dkiqF@xs z*>7~zPy%m#qOl2MS0)snC6OCwJzfJR1WYpgN8HhGg0}lpyB~yPbz!$`os4dnZd&fh0HAQ57{`xGO)8K2;$vXpOnn`_` z6okU3bt=#0J;RC}#6i&7P5+;*Mb|Ys+S^O`aS}nEj@6^+z#+9Q`ly=P%ue^b3KHxU z@J;aKe#4fq!UdDSR+cwm=&%h{`11zuPJzeK(YWHZbl;_v5A z8iILK`tkH<`!;)B4lxS3z4#U)?z&NpmmPSsB)F3l z4wDM%5o3}eo9@@xe&1qB6~}GRLG;u1=&Vh5RH&nij=Rnj+GJxiol&TTTp`IUW+ZGG z#mCwau0HXk(^_mzyCFJxN6*1G_S#EtQbA>~De~A7l9tbqVXiHMH)`c1j6&wzkEL3! zU!l$aI=clQCFkgITdJWQmVWF-m^mW3ww`v}d{OJMVx;4nH2kd>-z#i@Vq(+*N~Wd} zb-V;TXyUL1!yZ8>u{n!+jVb46xzcIuk;enJpzadk z@_J-^(YNl|BxjQ@x_d%?%fUpZ`>$S$v!hwOhIr0lQZ^w!dwmoVmZTw=`82>`m0Ns_ zB&J>`?;#kFs)4$^YHOU#44Yw1$yIUbCEE-;l-@4R`zzR(auXT~v3!pV}mgF8)We1Gy4tFUtN=Z!hWs6NjGSGR`q4@U~zS)N%|pZSwyi`>?v_+ z31~M4{BUTbCmN_^=3azL9SKUr!11+l)5Il8Xm zXawH7>rd{`MzmedgST5iqIXEoOq!j`d-r-w;@uGdjqyx*hPQ4Z(Ybs5T;JL@*m~k9 zqZ?OZ?H45fCtP-|}vS)Tz9Z_0nHd z^SkWKDSBK><@aL|*e2)d84lq8+K0e=Q%ZnW-T2LIF(B8DYS{c;^)mF(AkBvnxYcr+ zbRvw`P|_%~=aa^G-59>k<~Spgc#+nlUGfd_Gqzf}6T&AL2D|fqB@?05wa_4Y>T>=+ z0$I zI8pke9&4^Gw=2Lc-C}sY;h^&w!Kri33X$BsV{Q*{nm_C@-NS}Zd_ut@NN*+N*>9Bm zR1OsK^c+mQRP{Zp#rt)ApI8OwnH{hw&GG75TQu8RSVXQw3-IXapgmq{u)p;XsA@~> zyrSM5FQ!+SMvP3Lb7IcDK`IwfxxGBJ(bKplgm1$5;?%z_wPp7|oUChOoeF{by?4O& ziv!EXtejepgU{@no!%~qVb6>_HXiK@PY=xxwF-*PdDcxSgIe{8!lH5LbGs-aQ{9fP zS?*1-!|e2)=8@=j4}7gEHHl(x&?DPIP6JOBz-~voylVHej@UPj&tRg9X71ifOY?ci z0_J4ARjSB%K_s2*EkyrH??V^f#H55=+b8E;f^5ze7yqd##n!8QM!?>ne)ptJF|k|z zN%B(0<>_SG_UNZv9+8~=JH5krIu(vLv7x%kJw8@f53QJK6Ku`<)hKr*pxMfsQM>xn zsHg6Xf>m44{2)!yB8ws>1xu`{knD4`&MiVMoREXdWQs~-Adksw?rLijx1)+@0I8{4 z#G5vA8O4M$e#%;RD=<2>OQ9gDM+@8WZ zbPw5`X5orS{D4NvVWnfY~<70ys1;Tnpj<&*#*xSR?;36%s|?!3;R-hG0iK~2c}e(%;tj+W)+SGlv5s`!s) zCol!Ft@En%0M3PUWG1FkS-3o5uWh*1yQGs~xE6{(qyEA|k>n%_!WVY984~axSW6{& zWgolqnd5+2jegDcKNtkGyYLe!s$<46@@DZ!r&&#$%0A_k!O}G2RX3`XCSHmF+*;KZF>s?^)k!uhC2SF%rHl6uoBs0b zp8+TO8gBR!QK#f+f7yZ5LB6W_)u}jx55FFt;6Se{6RmyuX}20 z>e(m81vvCv_fjvrF;_FLEbIj^_i&A_O<1nz(SY=%@?W1(=xTgzN+CudQ`u-Ez zL9JCGF)_3T*jnP0HQ#HCZ|Ino6(N!oV0SrvtL3iRb@d`drpw{!LshWFhM2?%g`d&f zB1kyPyr9RJ#p*5jY)(1C%;~GI^*>`;Oa62a^6>B!sd3p%#)^9k!`OQfMCr_K@-SVH zelOkjuTUyqK4vSc{n$5L!0)xmfS}jxDdd1Me0ZE({^&tA>jO}2Rt?*@2IuT1Fv0Rz zQGC1s80)gw0kW-rO@k5d7zb7DVUf-+tT@;I);bNAxuh7kY&Al9j_D&H69S@oj7oRg z^`ixqR}IEyxBx7o$>`zkv?j->TRg4GI#RHG;nGU5dPCw6mE*`38$l6h2;2UyRnKj` z!=>bsS`eSyavorOc5`TxG|{|oJ3%jr2j?Z*V4!}$>f8BYsdmhlpJK>-rF~WMk(5)Y z+FuOBHX|Gm&8|>0Au8(tiKcH$Xsp$JuG`y;Jug7+<#DY4rB;@3f`P_wd{^Pc2nom3#H?sVQ8Tc6&a_FQ7GoN&esqfWB?R4eQrk>Hj~F&nTt;0z7;dH zCt|rXrn3uT(G zU=gq5ochNs54H<4ZGScYY{<=d!@G@C@{|Wm^!jZvG_#f0GykPCx-a*ncQ> zSpUnw5V0vTWbZYM^ug0#$`kLetv*++&#jsB3UhVKwtq3ekz@aIl^h=w6`fMO6k0MP zv3@kkrp%zXed1$#DAW;L(&zmXee{kSTcy#4za@b>I3vM|GgCtU$=eR=ecDp6D9Z^= zpOg{2#YLKH)quz_AtI|E=|QIYH0QbLNF8864fzoC0*=^)rg0a^+v|2Zx?Aq4OtSju zV1WA>9JCE40|O@)i%fNtZ{~{Z7;G@x{3=n_6@EAMUTAhyxKYP(bDz0#tt7O3&i~E~ zxHO;|O0?=}!c)=~Bn*sRALe3ZF*pvB+cV5luURz>vE3IivPfGviTdfO>opU-6G}*T ze3fIT)B;$y0iWI)k7inyHd4wQF-g*(SU4dyx^BB@+Lnet$-U8>8*!hj&5u8M82om> z&p~r>Jcoz(wYmaFB{THT3XL`&vhKRZAl=yZ&i8oR2e6CjJP0ApAAWLU;342!X>sSO zLAHes?P4ZFS8GvB*zvH1T6)lPpxwb{G-pUwQl>MOr2Q`=oh!%%?Od%jO4KNRoy_~L zP)>hcuNdynhTFa!4jNj&PfD@@%BL5Lovr0d2sMLd!G8gK@W(d7xWgML&)NvkE;_?% z74c09(5GK{-e3|XCQZH}fnBq-BAD)iecf;Yap$;}`9N$lOmu+K)E+7eijkRl>`CqV zWIFgzv3iGN@emqM*HORd!4zkXYv1RF#C6jX(s7}?9I$GdeUO6SmnVNK81e4>1*eg_ zd&10vs^FYp=^%iRCv5_y69{BiaM!u*fy91y#OPqExJRa3BHqp2-90i;Wud`-_w3FC zA}@Nm1EpxDdI!VyZQ^%UIFdVI4Y-4N2(w;g{ryklzIc_I`01BEBX2slEUxbI3M2} z6s`SP(I09@2uqw=ubZ|ptTtGK|5f6fW8@gH85O(^;i`%EgSe?K3r`M2+_oSe(frUg zH`{W=;Qri-l~fiWI>@75$9SBDGzMpdiRKgEX7b*X0;|jXt_bioS!TR|P|}4T4M~g%GfgVb2>MdrYu`aD&?Ou%2VS3o zat(Em1`jB2@K=Z$c%Ckg>+aUB_7C-)3f|8wE=tMCVdxBN{h8|LBr17$vaH&Ha?fOf zg?3_K{E-Rw{FFOeJ=JQ7g*;wG6OH!t$*Go?fN5aW#WUh5oq6vbzvU!f#YWfJkY)dD zw)E*R`|z6Z=re?;w>x1*tOv(cYSdDF-AHngdWP(~HlrDoGn1-X45!!22RUXf@u1r=0(Ny~e zq|e?OjZmNjj80}~xglD?N#qGc7&*fqSoVB=C7{ki+5;*oqd8_4u+M)4v%J~S#s1L3~t z3Xg4CzKgzhFJ;cwa=#C=!uXLgKSCV~o$t<^Qf%iKJ&}p?dXrlfDtnEOoP`^2mo0U* zTN5k{e8TVE+ePS>pqS)Oo_z}mSomlImwl|RuP^DBIAR;CBHG_F?yX3z#>I&fCmPaY z!m-A0y`)+mWI9=3JIW8l*tCm#0Z+2wxOOq3&~~{Vq2oiI*K$+_*-927 zaxJJM>tt>GZ4c>iy*uSv;iZcoir+u5&(rB{acw@ALI<(g6IRZ) z*r8ahcx%X217*jz(vp?a^1_v4P*pf7+4Uj%5%t>LB0{HdGE*kM44gzKK^HbOFvtuxhUnvZsyvHTV~|1^8R&~?^`#2bbfj9 zq<3@3iWLrzlu5dZ^6DRR*7_KK7?zkJ?*R~J@k@Y1F>NJ2w-I6kOGqB;p%7v25L;bY?F zxKu>v)9AXSj*RdzbkH)FA z7xsKGHQZGtyQQS+sWhGXPy7$8hE@|y`X2P4zZ!C{Y-AI_F%to;-lNq3T~~fUE@5=H zf=k2FSwFk;^+NOA3;LtHO`-kvop0$~gHQB`o(dsBhF*mlKeX(vud98qMScAneVx3{ zGFgt_2OShF3bW$)fr{n~^H7}*WIDG0rB1&D%w0 ziQ#zz!-J!9poVCSRjR``g8wwNyfQJuakM;TVIA!s@nqjZcZ2cYdkWn7xCP;;v)S=| zyVQd3geAm=2#0tv98wF&3QnpwSQf1ORNq-wf@9#{66VT&UrDPCaWfKagEOU%)p+#XKm2Ua8$dHb~)|y z-Ob{alXHAZifL)wVV4-lvuiw8?C!u1Ub=<5eL_|xq+&*N{}4Ww;x6L*X6b1|>fkTU zGAp-+Y~s7SQHd<>_^T_p!?6A$Emr#6H;j=TN`c7>gf4fmfY|1JQKzQZxPZp&n=?vE zh?|}kmI|&kP8YG}PkrpN0~D2cK*wIq!2Nri8QI$7h^5%AfxVu^G2yWrPoPYFC@d_x zUjB(Dv2dcke4R2zRg@6Fk_nwLC!{q9=lYvMno+t7&?cr{v zGG+PK4IXN8)&;-JiAKdvnFUWetPW~F&0ij5;x#0G8v;m{@#OJC9SZ>1z@^h2P$Fif zvEv6`I?qr1Kby#><^_$vm)^pQ)!SRm1K?dt3FKUA>R9+S22BV6(2z@91q(ua)2t*1 zjD&=Rt-eUym9RPulCL z-~k69;?Hn#@vGd;52kw9WSB^2z#qjmowg*+vzoA|txkxPH?r=I*j@PwaKv+5_h>lEK?cmG!NkDwfs~ka&Q zN^VZ|E&03b7Y_tVI_2?$R1$rpyP*Ms_$Wt$pkxo*=E@&6LHZgcHf`TE?!I%kpEtvK zSB&-&k+_b|uEpMsq!oITw%_;IjRm?{b--2GGyWGveS7;(KLAH1rghPmLG{m68lxlc z7e=KSyQT2Du}XniRc>(`=hsr&K4FjUAEH($nC0iUQ6_%rBlA|(j%%kZs~nWoDtL{x zCq@x6c-9)QZXNEM{rEO7HRM@|v)f#6*TCTVysdK!0S0)dADhDT0Ayxox&KYKSArG( zIae@*Qp@da^x`KrE9SOyx>}))qH+WT+lwSGPt3An{z3Q|+#0qOn3{XJ> z_9=QxNIpy}2Og4U%jx3PUn^Hch7UB)X8t-Q`3PE>;594QuOl#Yo`I}=^v?2p7-Zoa zz@*T6G*mIe?8hE66&eYM)uaLI9~{E8RI_7X*>~@Wl6abdxB!!0a3KKfk;evPi(l-DeFvLbCY#>AO zM2cecpgw=&0K}LA6)mid2RGOtj0s=8S=^A~Gfcd;$mpj!?#nV9vdnakaRm*Pf8B+m z;`f`g0gGC}J#q2|d!v!F`21}#_LUjYj%4Z&RR&=d$x6~{q+>fhSe7Z$@3E0Ed%7Z; zutb-A{er)7Bg~N&x+r@DhAy4*M+E{|fD3REgGbOow`ssl*>+_W4`i-*UY%G?URT&y z_lfvSJ7y9H5E8@zAJT{RftyqJ)tfWep&z_^?UNj5wvD~0a7bRdOBJhuXT3fI_NkSW zzW);Ftb9qm-oQ?*O!y!onJs1IO{PF3PoNaM$wLhL<}zQp-GS(Aq^UC^3Fq|-Cil9* zO#pf?FMA?%^30|N!58YZ)nv&uLZAWaOX-3%kj;XU@C`!xI>wa`MsC z&+Xxr%(=@h=Cc#{k_P)2S=lbr#xyMLDow^~WzxZ(kfeGZ1q~H zV3E!)Mnw56p8~X&fVz~3Nflm9-OnhX`jxrkmQEW6&|it)^SJ~JmS{)(8@393DlP*m zgZ@jo3y^RQiC)^!N=?fy3rlPkbpHc!j)l+Z2%fF>@(%fJG|d+u|0q?xQtsln|5<7rBhEuA-CcP3jTg3RtHpYl}*O_M2~EgpK!4&79=_ z6Hcu4LNhm11ZiSn-c*_9rhQpZ_}RAVUEyww^R;>aruVQYtY!=SlJGN$HpH-UDqGeq zc6_X5Uqsl7tkQkb+p-;|K=Wed3QI8HD_dAM# z3qU$%FBwbo@M{UlU3k-b_pC}vSJ{)?bydPZ5dh!J&42mcXOr1pJy*p{?O3v*S5xj! zPe#^dhyFNzNEl(A3GK^&$(#1om}JHe56V>^KfA~ ztBGeIDiSAcAljRx{(qka1>E+(0n`8T?0`rK;H2V5=+c1n09L=%WKs_AL&}rREahd2 z{D(Car;G5iy|J)rmB=t4H+T&U);OZQv48L0_qd{Mjk?fZ9j)DBna5l9dy`smU*hHe zPSE}-Obh23Yw#`Tz7BEAY>l`xaE&4WA&e z`xbEBFIgPezZe1(Det$^1~)ez+wJ)xo+ZoDh69L3lCJyAXJF8^%lLb12vud;tp8-^ zqmk<4$mpq??GFRF_TnbGb=+dq7cC@pt)=*QyW>Re=X-eTnf=a7TZ;wCt!`)BQWi_ zGZohFZQhpCwIuVCkQ}*o5UaT0II{opQb`8%a^5x0eUVNz*69OasynxWlBzF%y-k)I z%(uuVa6abX>8*xWH}v~^e4nLU%_O;{by#!!b-0flcX&;?aA)&ZHVeH{a1J4RheV1eT_os$Op?44 zg5uonvClB>wSv`|d`mgwgAjZE5`ZRuXD<+7?aZw-tk>rex8Yi8#|SuwYi1~{RBbRS zW#OspQYoLQP8Qz$)Gu?lC+)47@g-o|<+hzxj#*q~6qq|q>wES>*-F^>>8OzYUDBjF zK8KLe)Ag!33GI%eUVW75pCLSdQoB9nG$Np1YVRc@+uP+=aU&uxKA4|Vn@s29+6{2T zd;(MoI0Bj?Pm*|6&!|V{W;SINWSraV2blwf2R3Xm1*$-x1XU_JtI>ogN3V*=`k2tk zDSv&g89mq(p<%mx&plbhwhU#qE>{lbBaIq{+S=AzJ_7>uw{~bRmvgMpk{m{+gtb<( z*glFt!5qz8LrwA=(&$-9@L7NicI#NcKhb1jxl)5gxpYJ5l_;!Ow9_ix(G-7VCN*4L zodlVl-?t|0NW;2!t;)6hS|OhDZ*dmE8|jGa{=sJ=oad)2!cs5gqn-L=uYp>l&io}W zDjv^|9BSZR9N9rKcX_9+>y&DsEMU-pviI*iC8b%WjjoQS^zaJ5qG6Ct#j?+?#}$6H zWRaSI@1~>09EjY*h5ry7EUuv}bNvY`MzY6)cRrTtt$Qqq`_3PG4wI4Y03eLjQ=l7r z82-}nqrMSLYc-Dak4O*?UvJ!bM?9w~omlLx@l_k`SZu+Tuvc@1y_)e?RDVmfej1KW zOb>B5G7=^d%&qITF6N1MdR3q;l`zdbg`B85M=%cgnJm6ED-#A@n1QzQBz{!XZiIIu zFip;~dr4-NJhn6E76pY#pG}vOc>^PX8#1Hf&YQFfZwXW5p^s!P3##wzwEp=5HcTdY zU&NLUAhxtQAR;&`*~-K`AWD~|@D~asQdSaIC9@NsL&rPUOlHS)8 zGOV^lm?7`2V-FKw8b))WRG%YBgKVq7!_k7+P|?|^r||L@!|%$iP`mW+j%+PnK?LDd z4^;447y_Y?Li;)(oU>jzFT*5cgX-vV4vVR~y6TKx)Rd=DfVm1qYuEC(pLoq&F()lT z>A6W}z*AS+;fQ}UE%O_6@*{T>=@`*z0|*imz(z5x80&JtS|cKia$#hwPlS`pyojko zGLr}; zaY?l-g5nwFL23P@I9y|Moc*TXObwUJ{muydDnrF=3-X}Ocexqy++Wo@vtu&RiGt>| z0vub>Ow5n?{MaBBXt%$~66Ks~C^`^#1PX^QI>VpA66~b(5XK&c z{XI6#ov(b=b8vc`hjmV(#(s=!uvQ%WHNHXqqfkg^BsQ>ip-IHf<`=gTC z=psg1l0rbJe2{NynJz*3rf-+bFR4AtU(0E{b1b>|aNERc>-?2aeFmcWM{HNZKn8&T z3?r6WB)He^{FA6)Uaf9kXyKgaAF~XL0Mu0LlY7Gv2E2LIW7$Azuy+JKUKKuSf;}Qfa+RY01 z^TU67;HbRBR&A{drk69aUo6Hw_?f|F?li_d+`jWDHW!E3JlP-GI`c;}(_<#;&XcoT zl8QG|9o@co;qOY&)6IUEm4H}i=bCfc!k2%a z5oGqWzUHaVlo?OdysUZ^#@b$20&Lgw`D%5FCg3y#v_rq0w(l88KAZm!2lELK?*g7z z<6^}8ZV46is(%vFtT~i*1RDuIs4F9yqibGdg8v=$>PoS>7J5e!t~E3_7ey;KnM_G| z+6F<%YTG59n7o$J<0~=(z>RXXl>=gn!NYyFsC5foYyxX;^INi4FlSllNvs0Csh>~HmSz21q0ZdrV@|F=wr2BgtmA5#9 zUrm=*xcmriRoYz{8IMu)fzGFNyi|(DF7of)XRd5E!gFKAV9cO>0L1lZONs`kHuW4AJ(S$QVSPiSH72v1iAVND*I1D?k`(oA^IlwL#S7RMbnJ1-gCYUJ_g~jj z0WxSGz6EUlFXh^Qmv8^`&jj$xlFt9dR{58p6wi8rJe1A-#L#OgC=}V9-<&X?u($tP z>k*`#3#{P(FFh6(j=wr6#Pvxbks;iDZYPHDq)SV~&(a9iH&wjK_ zf8vTlvsG!yXO<*IsDDyHCA0P0w>LxnCohuR!dZeK{hpZ|D~DgK7^I=zi1 zo1|&Z6ALpViQ5D=%~47_b3PI(Tpf`-GRMTZ213#(tGNE|mfD z+*K9s-3kPoPs>DqME>&Pv!TbZ8XVP`f$Xz3k za;B3S<_01qtj{6pb;uug=)D@h$dz#!6_fOLSCOAaPh{8lFc|RPJh0cih-Cl4Hwd)% z%nsBPssR2pzR76jv8A>3>0@Vic+XE@xu}(uN$;i{@Om)l#PcRAlHGR2z1X?C69X*J zd{Wal+A7l$%?GhLeJALW58k{zhGD!ezs(JH2YKy$UqN}{8e0n8b=)B9E5RtvSFJ>C z%e{=?^1ECyH_+lmMQoPf-Rz1?G~d$0SzYZ-=Iwi{Cb^a4<#Q9AL(i>qk`}6sk?YuJ z9f!8$y}Pp8#mRBMS+^|DTj1S5Pok&Q&&1wAAaLu2Kk#;5x0NepPuVvzUz|?9uhe*7 z*_?KI*XDwkZ!e{QjFCNCE9W@a;?L!KJGQE2b>}UTBRo%;%F!R3C!X$G^D8PFSx52C z?@k{;WyhY|n%?KV&k&1KPExOWFi|gq#~Jp70Ep=j}h z0yC->l$uzzeeHx^8X6VqQJwKwug^=LJ29btGvDw_6&UHbN$Oq$*>@@ZbyR*j`2DI5 zzIPDikqCS;^2RZ~>!CKd#EOq!_gR*i;CvU)nGY;@w@KTlW$0qfLN^2r(ek~W5c=c` zr6)a9&&v|*rCQNsit7I|J3N<5j1xNlioSQg=2g=+JOhl-Kk*6yF_+joaByZ&Xl%~> zE~|hY_Hx$;?3d1=92(*GS2xMt8g%&d8^ZX(L7-Uo(A~~{qj{vCFvCLofiMY7>diy`>nxjT zB?T-8i(AEl1=lsyU%3iKc?nrBIk`Ww^i=N+_iZsPN zKR9u}M!*|P*@rr~Qh2_(liNLM&+@mgP6LW0`1=2|f%u;JuQ+QU6@0jHfSKP?{|e1Y zfC0s2)3O2}a~opA9rXrzd2xF5c@~=-sS8myd5%Qg;Lk-36cZWVi< z8p)wq@;Ba({)1?O5M0I}NChP&3gmsnSx+*NZ}<5!|DV>*JRa)3?c=xQM6zWkN)(FQ zz8m{eN+^5wB}|s=W8ax-L>;oz32`t8*#-%jQBk2##=gebV4c`h4Ek=gY6WYxMqT=4$ezMVTNIg9b446K5DMxryt}pu)_xuSSiNOLjmq z=0ne<)u-Jp5qn=9Q?@4T!!@~#22OXlR0mv@HPp4XYKB864GzO8f`!X|c;S(zOT$lYxHt&( zWZ`LO#?PHgOdk$u0(l~>Z-VWZf0X=rt7z-ybhI(gWe^C|RW5Jx;OO(ua#U`Fao$h$ zDwBK-l+5Loo)HLv_1`%4UPEXIN9p=J>}gEd!g_zY+2%|fIuE_jZsTY2Vu;&XPgE!_ zDJ(|t3Mn{45xKQCxrH1*B>-Y7-p%M-Z$u1PIJ6s$ z*6ukD{tq8L{m@76vrfC4M2^IxX9;gjNZ<2K>7=+ePO^t}mR+2&g=-JnVJGhuz^j>T$ zjR;~i;oKK3S+$4orCPaXw}O)F3D5QZw8jIRA0C_;bcCdrQq$fZ>;H;Z0~K2?_Cja1 zEFHqTh5!6CX+U4cMGNdmC~-{RYfR6%KniJLt1y-*6kES)qC9w-Rlt|@ebx~|g&4|9 zsQ1`F`w2sKYhI^Ub%Rp!OT>U7n6Yonle_yy9cxp({FYqh#m8*O&5T4eCI$q z*3)dD;fa}sfd9-L^U9=HBq{mFbs*OAje()Q6iIXDRrl3$4B#bsg;J(LzBYn_zpu5yVyC49CdLK zB2Uh`AXkS*=aRfFbhIs=0dh~E{9(bUX6K-VJ}`kGC(Pi@g6b*wZ$EHxt(pFVB?u{O z(|D61A|xDG(SEC!cCwLf(ONfyoEy^5K|K_E|O#bFU3KnVaTBCXjynFH-ZJ3L3H7rqVoG{k-KJZYJ4*OQ;j zxD9a>Y2!ihShk58OqOZ&JKf0eSNlV=c66v#YNN+gE&1|YB)q*%uTMT@h@Yi`cTy6R zj!T-4(bo9PWIgWe^*AbRfj_R0e^MnGl3INp`g4%FEYMU>ll&O70g|+tjm!1!?Vxio z7d3-3oR}ha7Ng^RC%IFv|7aU@Fa=~4d5Msc^iE8jcA^-v!iAP9jKGA|0_f;9zie$FFrLrd*j<9W;3mq>k%^%0n0)Wl=&K!Cg|AujpU{cX78yF!#@=(7AY# zbb{aXx0h<2+md6;PuPYNgC!U(9Bs|vr`Fn@x=b`$zHst)V3g}uTt}OCQ-k4=zlqvt z2ECsW$?OdBB=cD}<yFSy+c@PxbfMdm{!h4~-_av5-1~jMbW?ejeB0>S!32*8?j}_LOuxni_V_^J zRxCz?Lk+|Ti_s#=XY9SyW@-3gb;m4jNco$nK#^ij5uQ^6xftJyK!+neA%-?}>@(Y2 zdsJEeuM8<*IsLyU<&xGF`l-m!HG|Nl4#*|@HK{K1_O}&XNsiAPO*6gA5rYwOY!da; z!3BxMoODu?^TuJ!7tcCB&-0Xt&ccDP&aCx27!iYpy2QCAr=t_sjdeXa~1bdN^{i5}z ze~Cd*-So>RA@jSJn^PLs9Wd)#R}EO`fUX`rKZfu>8P9aGgGS@N_HKmFE$&Lr*CIA9 znCFd|*|tmFSH8^Ebv~#_^eY&kaY+qqs}`y@{_K|>i8@Rg+PwGo3#`X zUQpea7nB|tk<<=*nS`O@e`u=xEsgNV2ne-vA5uapdgFxmC)GRI__-i_4cfV+Fpc(s z{p&?4tyPFfib8kJgf!D)uDh;SJ}>_jii=&(f7W`~Eygu!5S%CcU0u1#6YpnBFL+8u zF%n)HL+De_u@uJ%Uak@#>jadwcR$&9$K21^_{bpWc{XQ4?arC;L+bT6R(@vQsk-aI zvhV1Zz3o((0)ho>zbX~pEfAFXho<4?E+*ZO&7|1YwYCZCLtguJdPGS@DsPo@=YIPV zPatuO8Sxe_Y-7yhu+1x`fnF2>(=ROQ+l;F6gWv$ItWSIa^xNFY2+lqCpEe3NUNnA; zDRw|H5CZSZpvy!TdUiGW1*KawT&T%U7~jKY0aCNVB_JsZ9I?^@kKe++-lMRlVXF7B z!VBEp=CW_R=FHUlX{K0vVl{}KCzKhOTlsSESWeay?u7lyP}ou^V_ouNM5NB))bK>I zMJd!jOD473nA-VW=%YVE@Y+vPEph3_i+cubC;@u8E<$;$Fsi0^hFf4F{GM(aIJ ztns7Baj-YvIaQX$j^0_e5Ast`{{drLjr}>8esj{vzYNyKh`xV)^D*ulotWEb+B~~) z-3Xr&{@#{ zn$m9|uAG?bOMDvFo3{<0_qWzng z{qq7%fxp*=9R!ns8e!(h*HVsa$GK~b)b~DEp2;B94Kqjl`~#Fq#$ood=6AYjZ5F;v zrmlDk8RdH-f0ZH$pG`3akJR!&qWVjC+qZk^^XnZzLcXgHx< zJThvX6`+{(g(VFcc+}YRp!F>?F1`x~Hwptu4MyTN6Qt;F1kbLJw3BdDVD>n1P9c1M z^O1u2s~e={uBjb7A0MKt1G7rx%oF||9zGchf|&1Y?jjzoodWz(+on7o>(2xm4b1Rm zZK6jhv;qWbv2A~;O)+HS3^srj>MHZ7bx#H>9Z07zp5}x#ok`q;u9oDs`NWLxt-!ty zRYmH)OE?DbM}$?aE4RYKHz3LjsQF7E8^waeNk$S*w-NVNLC~5)%d1qAEGfZzSu$=n za~Igj{1LOQN_9yI#!}3$Gg}sVZQRe@Z}t$)aHmWK1^)k5^8f!X_YzO_2?ujr4vH84 P7x>fHHPorpa)|v8caIK3 literal 0 HcmV?d00001 diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/BookingEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/BookingEndpoints.cs new file mode 100644 index 00000000..f07174eb --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/BookingEndpoints.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Mvc; + +public static class BookingEndpoints +{ + public static void MapBookingEndpoints(this WebApplication app) + { + app.MapGet("/bookings", async ([FromServices] IBookingRepository repo) => + { + var bookings = await repo.GetAllAsync(); + + var dtoList = bookings.Select(b => new BookingDto + { + CustomerId = b.CustomerId, + MovieId = b.MovieId, + BookedAt = b.BookedAt, + MovieTitle = b.Movie?.Title + }); + + return Results.Ok(dtoList); + }) + .WithTags("Bookings"); + + + app.MapPost("/bookings", async (CreateBookingDto dto, [FromServices] IBookingRepository repo) => + { + var booking = new Booking + { + CustomerId = dto.CustomerId, + MovieId = dto.MovieId + }; + + var createdBooking = await repo.CreateAsync(booking); + + var responseDto = new BookingDto + { + CustomerId = createdBooking.CustomerId, + MovieId = createdBooking.MovieId, + }; + + + return Results.Created( + $"/bookings/{booking.CustomerId}/{booking.MovieId}", + responseDto + ); + }) + .WithTags("Bookings"); + + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/LoginEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/LoginEndpoints.cs index afabd72f..0cb27824 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/LoginEndpoints.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/LoginEndpoints.cs @@ -1,8 +1,6 @@ using System.IdentityModel.Tokens.Jwt; using System.Text; using api_cinema_challenge.Data; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; @@ -14,8 +12,8 @@ public static void MapLoginEndpoints(this WebApplication app, IConfiguration con app.MapPost("/login", async (LoginRequest login, CinemaContext db) => { - var user = await db.Users.SingleOrDefaultAsync(u => u.Username == login.Username); - if (user == null || user.PasswordHash != login.Password) + var user = await db.Customers.SingleOrDefaultAsync(u => u.Name == login.Name); + if (user == null || user.Password != login.Password) return Results.Unauthorized(); var key = Encoding.UTF8.GetBytes(jwtSettings.GetValue("Key")); diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/RegisterEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/RegisterEndpoints.cs index c0bfde1c..9f626d91 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/RegisterEndpoints.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/RegisterEndpoints.cs @@ -1,18 +1,26 @@ using api_cinema_challenge.Data; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; public static class RegisterEndpoints { public static void MapRegisterEndpoints(this WebApplication app) { - app.MapPost("/register", async (User user, CinemaContext db) => + app.MapPost("/register", async (RegisterCustomerDto dto, CinemaContext db) => { - //password in plaintext :( - db.Users.Add(user); + var customer = new Customer + { + Name = dto.Name, + Email = dto.Email, + Password = dto.Password, + Phonenumber = dto.Phonenumber, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + db.Customers.Add(customer); await db.SaveChangesAsync(); - return Results.Ok(user); - }); + return Results.Ok(new { customer.Id, customer.Name, customer.Email }); + }); } -} \ No newline at end of file +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114838_bookingTable.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114838_bookingTable.Designer.cs new file mode 100644 index 00000000..19ac9a7c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114838_bookingTable.Designer.cs @@ -0,0 +1,206 @@ +// +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("20250825114838_bookingTable")] + partial class bookingTable + { + /// + 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("Booking", b => + { + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("BookedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CustomerId", "MovieId"); + + b.HasIndex("MovieId"); + + b.ToTable("Booking"); + }); + + modelBuilder.Entity("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("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"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Booking", b => + { + b.HasOne("Customer", "Customer") + .WithMany("Bookings") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Movie", "Movie") + .WithMany("Bookings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("Screening", b => + { + b.HasOne("Movie", "Movie") + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("Customer", b => + { + b.Navigation("Bookings"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Navigation("Bookings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114838_bookingTable.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114838_bookingTable.cs new file mode 100644 index 00000000..4698ca6d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114838_bookingTable.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class bookingTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Booking", + columns: table => new + { + CustomerId = table.Column(type: "integer", nullable: false), + MovieId = table.Column(type: "integer", nullable: false), + BookedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Booking", x => new { x.CustomerId, x.MovieId }); + table.ForeignKey( + name: "FK_Booking_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Booking_Movies_MovieId", + column: x => x.MovieId, + principalTable: "Movies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Booking_MovieId", + table: "Booking", + column: "MovieId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Booking"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120320_removedUserTable.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120320_removedUserTable.Designer.cs new file mode 100644 index 00000000..9f1b42fd --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120320_removedUserTable.Designer.cs @@ -0,0 +1,214 @@ +// +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("20250825120320_removedUserTable")] + partial class removedUserTable + { + /// + 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("Booking", b => + { + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("BookedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CustomerId", "MovieId"); + + b.HasIndex("MovieId"); + + b.ToTable("Booking"); + }); + + modelBuilder.Entity("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("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"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Booking", b => + { + b.HasOne("Customer", "Customer") + .WithMany("Bookings") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Movie", "Movie") + .WithMany("Bookings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("Screening", b => + { + b.HasOne("Movie", "Movie") + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("Customer", b => + { + b.Navigation("Bookings"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Navigation("Bookings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120320_removedUserTable.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120320_removedUserTable.cs new file mode 100644 index 00000000..582bb381 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120320_removedUserTable.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class removedUserTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PasswordHash", + table: "Customers", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Username", + table: "Customers", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PasswordHash", + table: "Customers"); + + migrationBuilder.DropColumn( + name: "Username", + table: "Customers"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120931_smallchange.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120931_smallchange.Designer.cs new file mode 100644 index 00000000..ba445f7d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120931_smallchange.Designer.cs @@ -0,0 +1,214 @@ +// +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("20250825120931_smallchange")] + partial class smallchange + { + /// + 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("Booking", b => + { + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("BookedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CustomerId", "MovieId"); + + b.HasIndex("MovieId"); + + b.ToTable("Booking"); + }); + + modelBuilder.Entity("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("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"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Booking", b => + { + b.HasOne("Customer", "Customer") + .WithMany("Bookings") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Movie", "Movie") + .WithMany("Bookings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("Screening", b => + { + b.HasOne("Movie", "Movie") + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("Customer", b => + { + b.Navigation("Bookings"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Navigation("Bookings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120931_smallchange.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120931_smallchange.cs new file mode 100644 index 00000000..a081bede --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825120931_smallchange.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class smallchange : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "PasswordHash", + table: "Customers", + newName: "Password"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Password", + table: "Customers", + newName: "PasswordHash"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825121333_smallchange3.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825121333_smallchange3.Designer.cs new file mode 100644 index 00000000..b3024b9c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825121333_smallchange3.Designer.cs @@ -0,0 +1,214 @@ +// +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("20250825121333_smallchange3")] + partial class smallchange3 + { + /// + 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("Booking", b => + { + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("BookedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CustomerId", "MovieId"); + + b.HasIndex("MovieId"); + + b.ToTable("Booking"); + }); + + modelBuilder.Entity("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phonenumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("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"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Booking", b => + { + b.HasOne("Customer", "Customer") + .WithMany("Bookings") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Movie", "Movie") + .WithMany("Bookings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("Screening", b => + { + b.HasOne("Movie", "Movie") + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("Customer", b => + { + b.Navigation("Bookings"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Navigation("Bookings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825121333_smallchange3.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825121333_smallchange3.cs new file mode 100644 index 00000000..bb10b54c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825121333_smallchange3.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class smallchange3 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Phone", + table: "Customers", + newName: "Phonenumber"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Phonenumber", + table: "Customers", + newName: "Phone"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825124543_smallchange4.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825124543_smallchange4.Designer.cs new file mode 100644 index 00000000..a36352e1 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825124543_smallchange4.Designer.cs @@ -0,0 +1,214 @@ +// +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("20250825124543_smallchange4")] + partial class smallchange4 + { + /// + 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("Booking", b => + { + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("BookedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CustomerId", "MovieId"); + + b.HasIndex("MovieId"); + + b.ToTable("Bookings"); + }); + + modelBuilder.Entity("Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phonenumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("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"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Booking", b => + { + b.HasOne("Customer", "Customer") + .WithMany("Bookings") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Movie", "Movie") + .WithMany("Bookings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("Screening", b => + { + b.HasOne("Movie", "Movie") + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("Customer", b => + { + b.Navigation("Bookings"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Navigation("Bookings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825124543_smallchange4.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825124543_smallchange4.cs new file mode 100644 index 00000000..408a2df0 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825124543_smallchange4.cs @@ -0,0 +1,102 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class smallchange4 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Booking_Customers_CustomerId", + table: "Booking"); + + migrationBuilder.DropForeignKey( + name: "FK_Booking_Movies_MovieId", + table: "Booking"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Booking", + table: "Booking"); + + migrationBuilder.RenameTable( + name: "Booking", + newName: "Bookings"); + + migrationBuilder.RenameIndex( + name: "IX_Booking_MovieId", + table: "Bookings", + newName: "IX_Bookings_MovieId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Bookings", + table: "Bookings", + columns: new[] { "CustomerId", "MovieId" }); + + migrationBuilder.AddForeignKey( + name: "FK_Bookings_Customers_CustomerId", + table: "Bookings", + column: "CustomerId", + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Bookings_Movies_MovieId", + table: "Bookings", + column: "MovieId", + principalTable: "Movies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Bookings_Customers_CustomerId", + table: "Bookings"); + + migrationBuilder.DropForeignKey( + name: "FK_Bookings_Movies_MovieId", + table: "Bookings"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Bookings", + table: "Bookings"); + + migrationBuilder.RenameTable( + name: "Bookings", + newName: "Booking"); + + migrationBuilder.RenameIndex( + name: "IX_Bookings_MovieId", + table: "Booking", + newName: "IX_Booking_MovieId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Booking", + table: "Booking", + columns: new[] { "CustomerId", "MovieId" }); + + migrationBuilder.AddForeignKey( + name: "FK_Booking_Customers_CustomerId", + table: "Booking", + column: "CustomerId", + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Booking_Movies_MovieId", + table: "Booking", + column: "MovieId", + principalTable: "Movies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs index 51c65d76..1da9d55f 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -22,6 +22,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Booking", b => + { + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("BookedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CustomerId", "MovieId"); + + b.HasIndex("MovieId"); + + b.ToTable("Bookings"); + }); + modelBuilder.Entity("Customer", b => { b.Property("Id") @@ -43,7 +61,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("Phone") + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phonenumber") .IsRequired() .HasColumnType("text"); @@ -52,6 +74,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasDefaultValueSql("CURRENT_TIMESTAMP"); + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + b.HasKey("Id"); b.ToTable("Customers"); @@ -140,6 +166,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); + modelBuilder.Entity("Booking", b => + { + b.HasOne("Customer", "Customer") + .WithMany("Bookings") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Movie", "Movie") + .WithMany("Bookings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Movie"); + }); + modelBuilder.Entity("Screening", b => { b.HasOne("Movie", "Movie") @@ -150,6 +195,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Movie"); }); + + modelBuilder.Entity("Customer", b => + { + b.Navigation("Bookings"); + }); + + modelBuilder.Entity("Movie", b => + { + b.Navigation("Bookings"); + }); #pragma warning restore 612, 618 } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Booking.cs b/api-cinema-challenge/api-cinema-challenge/Models/Booking.cs new file mode 100644 index 00000000..d27d9e01 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Booking.cs @@ -0,0 +1,10 @@ +public class Booking +{ + public int CustomerId { get; set; } + public Customer Customer { get; set; } = null!; + + public int MovieId { get; set; } + public Movie Movie { get; set; } = null!; + + public DateTime BookedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs index 0802bdea..972846d0 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -1,9 +1,18 @@ public class Customer { public int Id { get; set; } + + // Login-related + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + + // Profile-related public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; - public string Phone { get; set; } = string.Empty; + public string Phonenumber { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public List Bookings { get; set; } = new(); } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/LoginRequest.cs b/api-cinema-challenge/api-cinema-challenge/Models/LoginRequest.cs index 15bf64ce..7240ce14 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/LoginRequest.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/LoginRequest.cs @@ -1,5 +1,5 @@ public class LoginRequest { - public string Username { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; } \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs index f714196e..4beb17bd 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -7,4 +7,5 @@ public class Movie public string RuntimeMins { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public List Bookings { get; set; } = new(); } diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index cf6419b8..3b96a162 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -12,6 +12,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Add services to the container. builder.Services.AddEndpointsApiExplorer(); @@ -87,6 +88,7 @@ app.MapMovieEndpoints(); app.MapScreeningEndpoints(); app.MapRegisterEndpoints(); +app.MapBookingEndpoints(); app.MapLoginEndpoints(builder.Configuration); app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/BookingRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/BookingRepository.cs new file mode 100644 index 00000000..d5e6c619 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/BookingRepository.cs @@ -0,0 +1,28 @@ +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; + + +public class BookingRepository : IBookingRepository +{ + private readonly CinemaContext _context; + + public BookingRepository(CinemaContext context) + { + _context = context; + } + + + public async Task> GetAllAsync() + { + return await _context.Bookings + .Include(s => s.Movie) + .ToListAsync(); + } + + public async Task CreateAsync(Booking bookings) + { + _context.Bookings.Add(bookings); + await _context.SaveChangesAsync(); + return bookings; + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs index 462203f1..715278b6 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs @@ -35,7 +35,7 @@ public async Task CreateAsync(Customer customer) existingCustomer.Name = customer.Name; existingCustomer.Email = customer.Email; - existingCustomer.Phone = customer.Phone; + existingCustomer.Phonenumber = customer.Phonenumber; existingCustomer.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/IBookingRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/IBookingRepository.cs new file mode 100644 index 00000000..3fe9a910 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IBookingRepository.cs @@ -0,0 +1,5 @@ +public interface IBookingRepository +{ + Task> GetAllAsync(); + Task CreateAsync(Booking screening); +} From 9fbad214edb2ea5df7568537b5216b7e0b6fb450 Mon Sep 17 00:00:00 2001 From: Chris Sivert Sylte Date: Tue, 26 Aug 2025 14:01:31 +0200 Subject: [PATCH 5/6] added dockerfile --- README.md | 71 +++++-------------- .../api-cinema-challenge/Dockerfile | 29 ++++++++ .../api-cinema-challenge/Program.cs | 9 ++- 3 files changed, 53 insertions(+), 56 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Dockerfile diff --git a/README.md b/README.md index 3612a43c..71b4901b 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,28 @@ -# Cinema Booking API Challenge + +# C# Docker Day 1 Exercise ## Learning Objectives -- Use ASP.NET and Entity Framework to build a RESTful API -- Use object-oriented programming to manage source code complexity -- Use an API client (Postman, Insomnia, etc.) to test-drive code -## Instructions +- Complete & Deploy the exercise.wwwapi API project to a Docker Container -It's time to use everything you've learned up to this point! Your task is to build a complete API in C# using ASP.NET and Entity Framework. Treat this challenge like a real project, not just an exercise to complete. Take time to read documentation, experiment & discuss ideas with your peers and teachers. +## Instructions 1. Fork this repository 2. Clone your fork to your machine -3. Open the api-cinema-challenge solution with Visual Studio -4. Create appsettings.json with your own db credentials. Note that the Data folder already contains a CinemaContext which you may use to add your DbSets. -5. Create a complete ERD to describe the model of your data and the relationships between each entity -6. Check the Program.cs and make any changes needed before starting. -7. Separate out your code in the Data context with seeders, Models, Repository layer, Endpoints layer (controllers). -8. Your task is to develop the API that [satisfies this API spec](https://boolean-uk.github.io/csharp-api-cinema-challenge/) -9. Finally ensure that you have implemented some basic security on this exercise. Each endpoint should be decorated with [Authorize] and you should have a login/register endpoint to get the JWT. - - -Pay close attention to the details of each endpoint. How you choose to implement the solution is up to you, there are no wrong answers, but the inputs and outputs must match the provided API documentation exactly. - -**Security** -## Extensions - -[Here is an extension API spec.](https://boolean-uk.github.io/csharp-api-cinema-challenge/extensions) - -It contains a few new routes, different approaches to data mutation and, most importantly, an entirely different response format for each request. - - -### Tips -- Beware of cyclical Json and Db Entities reference - - Use decorators to ignore model values to json ignore - - You could create different Data Transfer Objects to avoid those dependencies -Entity with cyclical dependance: -```json -{ - "id": 0, - "name": "string", - "otherEntity": { - "id": 0, - "entity": { // <------- Root entity is repeated here, resulting for child entity being repeated and goes on and on. - "id": 0, - "otherEntity" { - ...The whole structure repeats infinitely... - } - } - } -} -``` +3. Open the project -Current: -- Create the ERD -- Create models -- Create Controllers with routes to satisfy the API -- Create Relationships between the components as shown in the ERD +## Core and Extension -## Useful Resources +Dockerize an existing .NET Core Web API project, you may use this project, an existing webapi project or start a new one. If you use an existing one, just remove this project and delete from the directory, copy the project into the root and add to the solution. +- Ensure your application has at least a GET, POST, PUT and DELETE +- Ensure your application has at least 2 Entities and 2 tables in the database +- Your API should connect to an [Neon](https://neon.tech) database instance that can be used for storing the data. +- Be consistant and choose one of the following as a response from your endpoints: + - an anonymous object + - a custom DTO + - Automapper with DTO -You'll need to do a fair amount of research in order to complete this challenge. Make liberal use of StackOverflow, search engines, YouTube and the teaching team. +Create a `Dockerfile` and any other associated files to allow you to deploy the application using a Docker Container. -Here are some reference documentation links that will be useful: +Make sure your `appsettings.json` file is on `.gitignore` so that it doesn't contain your private database connection strings. -- [Enitity Framework Core Documentation](https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli) -- [model-json-ignore-properties](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/ignore-properties?pivots=dotnet-7-0) diff --git a/api-cinema-challenge/api-cinema-challenge/Dockerfile b/api-cinema-challenge/api-cinema-challenge/Dockerfile new file mode 100644 index 00000000..dffbf425 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Dockerfile @@ -0,0 +1,29 @@ +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copy csproj and restore dependencies +COPY *.csproj ./ +RUN dotnet restore + +# Copy everything else and build +COPY . . +RUN dotnet publish -c Release -o /app/publish + +# Runtime stage +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final +WORKDIR /app +COPY --from=build /app/publish . + +# Expose port +EXPOSE 8080 + +# Run your app +ENTRYPOINT ["dotnet", "api-cinema-challenge.dll"] + + +# Create the image with +# sudo docker build -t dockerimage . + +# Run a container +# sudo docker run -p 8080:8080 -e ASPNETCORE_URLS="http://+:8080" -e DOTNET_ENVIRONMENT=Development dockerimage \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 3b96a162..670bd1e6 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -18,6 +18,13 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { + c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + { + Title = "API Cinema Challenge", + Version = "v1" + }); + + // JWT config c.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme { Name = "Authorization", @@ -43,7 +50,7 @@ } }); }); -builder.Services.AddDbContext(); + // JWT config var jwtSettings = builder.Configuration.GetSection("Jwt"); From 31a1046b5910a30053bee7d5aa977ccf0cbf190a Mon Sep 17 00:00:00 2001 From: Chrissivert <122440126+Chrissivert@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:39:58 +0200 Subject: [PATCH 6/6] Update README.md fixed readme to be cinema not docker --- README.md | 71 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 71b4901b..3612a43c 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,67 @@ - -# C# Docker Day 1 Exercise +# Cinema Booking API Challenge ## Learning Objectives - -- Complete & Deploy the exercise.wwwapi API project to a Docker Container +- Use ASP.NET and Entity Framework to build a RESTful API +- Use object-oriented programming to manage source code complexity +- Use an API client (Postman, Insomnia, etc.) to test-drive code ## Instructions +It's time to use everything you've learned up to this point! Your task is to build a complete API in C# using ASP.NET and Entity Framework. Treat this challenge like a real project, not just an exercise to complete. Take time to read documentation, experiment & discuss ideas with your peers and teachers. + 1. Fork this repository 2. Clone your fork to your machine -3. Open the project +3. Open the api-cinema-challenge solution with Visual Studio +4. Create appsettings.json with your own db credentials. Note that the Data folder already contains a CinemaContext which you may use to add your DbSets. +5. Create a complete ERD to describe the model of your data and the relationships between each entity +6. Check the Program.cs and make any changes needed before starting. +7. Separate out your code in the Data context with seeders, Models, Repository layer, Endpoints layer (controllers). +8. Your task is to develop the API that [satisfies this API spec](https://boolean-uk.github.io/csharp-api-cinema-challenge/) +9. Finally ensure that you have implemented some basic security on this exercise. Each endpoint should be decorated with [Authorize] and you should have a login/register endpoint to get the JWT. + + +Pay close attention to the details of each endpoint. How you choose to implement the solution is up to you, there are no wrong answers, but the inputs and outputs must match the provided API documentation exactly. + +**Security** +## Extensions + +[Here is an extension API spec.](https://boolean-uk.github.io/csharp-api-cinema-challenge/extensions) + +It contains a few new routes, different approaches to data mutation and, most importantly, an entirely different response format for each request. + + +### Tips +- Beware of cyclical Json and Db Entities reference + - Use decorators to ignore model values to json ignore + - You could create different Data Transfer Objects to avoid those dependencies +Entity with cyclical dependance: +```json +{ + "id": 0, + "name": "string", + "otherEntity": { + "id": 0, + "entity": { // <------- Root entity is repeated here, resulting for child entity being repeated and goes on and on. + "id": 0, + "otherEntity" { + ...The whole structure repeats infinitely... + } + } + } +} +``` -## Core and Extension +Current: +- Create the ERD +- Create models +- Create Controllers with routes to satisfy the API +- Create Relationships between the components as shown in the ERD -Dockerize an existing .NET Core Web API project, you may use this project, an existing webapi project or start a new one. If you use an existing one, just remove this project and delete from the directory, copy the project into the root and add to the solution. -- Ensure your application has at least a GET, POST, PUT and DELETE -- Ensure your application has at least 2 Entities and 2 tables in the database -- Your API should connect to an [Neon](https://neon.tech) database instance that can be used for storing the data. -- Be consistant and choose one of the following as a response from your endpoints: - - an anonymous object - - a custom DTO - - Automapper with DTO +## Useful Resources -Create a `Dockerfile` and any other associated files to allow you to deploy the application using a Docker Container. +You'll need to do a fair amount of research in order to complete this challenge. Make liberal use of StackOverflow, search engines, YouTube and the teaching team. -Make sure your `appsettings.json` file is on `.gitignore` so that it doesn't contain your private database connection strings. +Here are some reference documentation links that will be useful: +- [Enitity Framework Core Documentation](https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli) +- [model-json-ignore-properties](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/ignore-properties?pivots=dotnet-7-0)