From 501a9f05e73eebb282c3bf2aec2c0a69c485037b Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 22 Aug 2025 09:35:04 +0200 Subject: [PATCH 01/16] feat: initial classes --- .../api-cinema-challenge/Data/Seeder.cs | 6 ++++++ .../Endpoints/CustomerEndpoint.cs | 6 ++++++ .../Endpoints/MovieEndpoint.cs | 6 ++++++ .../Endpoints/ScreeningEndpoint.cs | 6 ++++++ .../Endpoints/TicketEndpoint.cs | 6 ++++++ .../Factories/CustomerFactory.cs | 6 ++++++ .../Factories/MovieFactory.cs | 6 ++++++ .../Factories/ScreeningFactory.cs | 6 ++++++ .../api-cinema-challenge/Factories/Ticket.cs | 6 ++++++ .../api-cinema-challenge/Models/Customer.cs | 6 ++++++ .../api-cinema-challenge/Models/Movie.cs | 6 ++++++ .../api-cinema-challenge/Models/Screening.cs | 6 ++++++ .../api-cinema-challenge/Models/Ticket.cs | 6 ++++++ .../api-cinema-challenge/Program.cs | 21 +++++++++++++++---- .../Interfaces/ICustomerRepository.cs | 6 ++++++ .../Repository/Interfaces/IMovieRepository.cs | 6 ++++++ .../Interfaces/IScreeningRepository.cs | 6 ++++++ .../Interfaces/ITicketRepository.cs | 6 ++++++ 18 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Factories/Ticket.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Customer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Movie.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Screening.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs diff --git a/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs new file mode 100644 index 00000000..71313cae --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Data +{ + public class Seeder + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs new file mode 100644 index 00000000..df19497e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Endpoints +{ + public class CustomerEndpoint + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs new file mode 100644 index 00000000..897b8e5c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Endpoints +{ + public class MovieEndpoint + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs new file mode 100644 index 00000000..4c4f95d9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Endpoints +{ + public class ScreeningEndpoint + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs new file mode 100644 index 00000000..8b047e82 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Endpoints +{ + public class TicketEndpoint + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs new file mode 100644 index 00000000..eba9b68f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Factories +{ + public class CustomerFactory + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs new file mode 100644 index 00000000..02d44b92 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Factories +{ + public class MovieFactory + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs new file mode 100644 index 00000000..08891939 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Factories +{ + public class ScreeningFactory + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Factories/Ticket.cs new file mode 100644 index 00000000..d1e041ee --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Factories/Ticket.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Factories +{ + public class Ticket + { + } +} 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..01ef7c78 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Models +{ + public class Customer + { + } +} 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..447805f7 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Models +{ + public class Movie + { + } +} 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..a3d0aab6 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Models +{ + public class Screening + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs new file mode 100644 index 00000000..0ca6ad1b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Models +{ + public class Ticket + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..2dc1787c 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,10 +1,16 @@ using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; +using System.Diagnostics; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Options; +using System.Diagnostics; +using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddOpenApi(); builder.Services.AddDbContext(); var app = builder.Build(); @@ -12,9 +18,16 @@ // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); + app.MapOpenApi(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/openapi/v1.json", "Demo API"); + }); + app.MapScalarApiReference(); } app.UseHttpsRedirection(); + +// endpoints configuration + app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs new file mode 100644 index 00000000..3e5b4167 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Repository.Interfaces +{ + public interface ICustomerRepository + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs new file mode 100644 index 00000000..36fce701 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Repository.Interfaces +{ + public interface IMovieRepository + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs new file mode 100644 index 00000000..d1fe1399 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Repository.Interfaces +{ + public interface IScreeningRepository + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs new file mode 100644 index 00000000..2ac9acf3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Repository.Interfaces +{ + public interface ITicketRepository + { + } +} From bc3628e0cf181d4fbbf2a174e43c418cec34a1a0 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 22 Aug 2025 10:03:57 +0200 Subject: [PATCH 02/16] feat: authentication setup --- api-cinema-challenge/api-cinema-challenge/Program.cs | 6 ++++++ .../api-cinema-challenge/api-cinema-challenge.csproj | 3 ++- .../api-cinema-challenge/jwt appsettings.txt | 12 ++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 2dc1787c..35a586c8 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -11,8 +11,14 @@ // Add services to the container. builder.Services.AddOpenApi(); + +// Dependency injection builder.Services.AddDbContext(); +// security +builder.Services.AddAuthentication().AddJwtBearer(); +builder.Services.AddAuthorization(); + var app = builder.Build(); // Configure the HTTP request pipeline. 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..4e306eae 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -1,10 +1,11 @@ - + net9.0 enable enable api_cinema_challenge + 8499e9e9-9306-422f-a58d-332dfc8c5416 diff --git a/api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt b/api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt new file mode 100644 index 00000000..a236a2fe --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt @@ -0,0 +1,12 @@ +"Authentication": { + "DefaultScheme": "LocalAuthIssuer", + "Schemes": { + "Bearer": { + "ValidAudiences": [ + "https://localhost:7259", + "http://localhost:5259" + ], + "ValidIssuer": "dotnet-user-jwts" + } + } + }, \ No newline at end of file From d9f9dfbc96bab2e0d7a65db68ebcab745ee89d94 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 22 Aug 2025 10:12:06 +0200 Subject: [PATCH 03/16] feat: data models --- .../api-cinema-challenge/Models/Customer.cs | 4 ++++ api-cinema-challenge/api-cinema-challenge/Models/Movie.cs | 7 +++++++ .../api-cinema-challenge/Models/Screening.cs | 6 ++++++ api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs | 6 ++++++ api-cinema-challenge/api-cinema-challenge/Program.cs | 2 +- 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs index 01ef7c78..3ff90f2a 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -2,5 +2,9 @@ { public class Customer { + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs index 447805f7..7590d3c6 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -2,5 +2,12 @@ { public class Movie { + public int Id { get; set; } + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + public ICollection Screenings { get; set; } + } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs index a3d0aab6..bfd43c95 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -2,5 +2,11 @@ { public class Screening { + public int Id { get; set; } + public int MovieId { get; set; } + public Movie Movie { get; set; } + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs index 0ca6ad1b..8199e608 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -2,5 +2,11 @@ { public class Ticket { + public int Id { get; set; } + public int CustomerId { get; set; } + public Customer Customer { get; set; } + public int ScreeningId { get; set; } + public Screening Screening { get; set; } + public int NumSeats { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 35a586c8..a791838a 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -17,7 +17,7 @@ // security builder.Services.AddAuthentication().AddJwtBearer(); -builder.Services.AddAuthorization(); +//builder.Services.AddAuthorization(); var app = builder.Build(); From 1de5d715d7ebc5b7187a4c718ce67705e15fd136 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 22 Aug 2025 10:17:33 +0200 Subject: [PATCH 04/16] feat: cinema context --- .../Data/CinemaContext.cs | 19 +++++++++---------- .../api-cinema-challenge/Program.cs | 8 +++++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index ad4fe854..7acbfcad 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,26 +1,25 @@ -using Microsoft.EntityFrameworkCore; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; namespace api_cinema_challenge.Data { public class CinemaContext : DbContext { - private string _connectionString; 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) - { - optionsBuilder.UseNpgsql(_connectionString); } protected override void OnModelCreating(ModelBuilder modelBuilder) { + // relations + // seeder } + + public DbSet Customers { get; set; } + public DbSet Movies { get; set; } + public DbSet Screenings { get; set; } + public DbSet Tickets { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index a791838a..b1f61ecb 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -13,7 +13,13 @@ builder.Services.AddOpenApi(); // Dependency injection -builder.Services.AddDbContext(); +//builder.Services.AddDbContext(); +builder.Services.AddDbContext(options => { + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")) + .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); + options.LogTo(message => Debug.WriteLine(message)); + options.EnableSensitiveDataLogging(); +}); // security builder.Services.AddAuthentication().AddJwtBearer(); From de87cd52dfb7a3ab0c9b73b90191bd3a98865117 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 22 Aug 2025 10:55:06 +0200 Subject: [PATCH 05/16] feat: seeder --- .../api-cinema-challenge/Data/Seeder.cs | 62 ++++++++++++++++++- .../api-cinema-challenge/Models/Customer.cs | 1 + .../api-cinema-challenge/Models/Movie.cs | 2 +- .../api-cinema-challenge/Models/Screening.cs | 1 + 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs index 71313cae..03870f27 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs @@ -1,6 +1,66 @@ -namespace api_cinema_challenge.Data +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Data { public class Seeder { + private List _customers = new(); + private List _movies = new(); + private List _screenings = new(); + private List _tickets = new(); + + public void Seed() + { + var customer1 = new Customer() { Id = 1, Name = "", Email = "", Phone = "" }; + var customer2 = new Customer() { Id = 2, Name = "", Email = "", Phone = "" }; + var customer3 = new Customer() { Id = 3, Name = "", Email = "", Phone = "" }; + var customer4 = new Customer() { Id = 4, Name = "", Email = "", Phone = "" }; + var customer5 = new Customer() { Id = 5, Name = "", Email = "", Phone = "" }; + + var movie1 = new Movie() { Id = 1, Title = "", Rating = "", Description = "", RuntimeMins = 60 }; + var movie2 = new Movie() { Id = 2, Title = "", Rating = "", Description = "", RuntimeMins = 60 }; + var movie3 = new Movie() { Id = 3, Title = "", Rating = "", Description = "", RuntimeMins = 60 }; + var movie4 = new Movie() { Id = 4, Title = "", Rating = "", Description = "", RuntimeMins = 60 }; + var movie5 = new Movie() { Id = 5, Title = "", Rating = "", Description = "", RuntimeMins = 60 }; + + var screening1 = new Screening() { Id = 1, MovieId = 1, ScreenNumber = 1, Capacity = 50, StartsAt = DateTime.UtcNow }; + var screening2 = new Screening() { Id = 2, MovieId = 1, ScreenNumber = 1, Capacity = 50, StartsAt = DateTime.UtcNow }; + + var ticket1 = new Ticket() { Id = 1, ScreeningId = 1, CustomerId = 1, NumSeats = 1 }; + var ticket2 = new Ticket() { Id = 2, ScreeningId = 1, CustomerId = 2, NumSeats = 1 }; + var ticket3 = new Ticket() { Id = 3, ScreeningId = 1, CustomerId = 3, NumSeats = 1 }; + + var ticket4 = new Ticket() { Id = 4, ScreeningId = 2, CustomerId = 1, NumSeats = 1 }; + var ticket5 = new Ticket() { Id = 5, ScreeningId = 2, CustomerId = 2, NumSeats = 1 }; + var ticket6 = new Ticket() { Id = 6, ScreeningId = 2, CustomerId = 3, NumSeats = 1 }; + + _customers.Add(customer1); + _customers.Add(customer2); + _customers.Add(customer3); + _customers.Add(customer4); + _customers.Add(customer5); + + _movies.Add(movie1); + _movies.Add(movie2); + _movies.Add(movie3); + _movies.Add(movie4); + _movies.Add(movie5); + + _screenings.Add(screening2); + _screenings.Add(screening2); + + _tickets.Add(ticket1); + _tickets.Add(ticket2); + _tickets.Add(ticket3); + _tickets.Add(ticket4); + _tickets.Add(ticket5); + _tickets.Add(ticket6); + + } + + public List Customer { get { return _customers; } } + public List Movies { get { return _movies; } } + public List Screenings { get { return _screenings; } } + public List Tickets { get { return _tickets; } } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs index 3ff90f2a..9620123b 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -6,5 +6,6 @@ public class Customer public string Name { get; set; } public string Email { get; set; } public string Phone { get; set; } + public ICollection Tickets { get; set; } = new List(); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs index 7590d3c6..4c0b02a9 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -7,7 +7,7 @@ public class Movie public string Rating { get; set; } public string Description { get; set; } public int RuntimeMins { get; set; } - public ICollection Screenings { get; set; } + public ICollection Screenings { get; set; } = new List(); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs index bfd43c95..ae10ac90 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -8,5 +8,6 @@ public class Screening public int ScreenNumber { get; set; } public int Capacity { get; set; } public DateTime StartsAt { get; set; } + public ICollection Tickets { get; set; } = new List(); } } From 40bc5c5e0968ff1a3682d6534616cb986e3248d6 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 22 Aug 2025 11:06:04 +0200 Subject: [PATCH 06/16] feat: program jwt --- .../api-cinema-challenge/Program.cs | 67 ++++++++++++++++++- .../api-cinema-challenge/jwt appsettings.txt | 23 +++---- 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index b1f61ecb..ab775bb5 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,17 +1,49 @@ using api_cinema_challenge.Data; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; -using System.Diagnostics; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Options; -using System.Diagnostics; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; using Scalar.AspNetCore; +using System.Diagnostics; +using System.Diagnostics; +using System.Text; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddOpenApi(); +builder.Services.AddSwaggerGen(option => +{ + option.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); + option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter a valid token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + option.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); +}); + // Dependency injection //builder.Services.AddDbContext(); builder.Services.AddDbContext(options => { @@ -22,7 +54,33 @@ }); // security -builder.Services.AddAuthentication().AddJwtBearer(); +// These will eventually be moved to a secrets file, but for alpha development appsettings is fine +var validIssuer = builder.Configuration.GetValue("JwtTokenSettings:ValidIssuer"); +var validAudience = builder.Configuration.GetValue("JwtTokenSettings:ValidAudience"); +var symmetricSecurityKey = builder.Configuration.GetValue("JwtTokenSettings:SymmetricSecurityKey"); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +}).AddJwtBearer(options => +{ + options.IncludeErrorDetails = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ClockSkew = TimeSpan.Zero, + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = validIssuer, + ValidAudience = validAudience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + }; +}); //builder.Services.AddAuthorization(); var app = builder.Build(); @@ -40,6 +98,9 @@ app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); + // endpoints configuration app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt b/api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt index a236a2fe..17673d93 100644 --- a/api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt +++ b/api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt @@ -1,12 +1,11 @@ -"Authentication": { - "DefaultScheme": "LocalAuthIssuer", - "Schemes": { - "Bearer": { - "ValidAudiences": [ - "https://localhost:7259", - "http://localhost:5259" - ], - "ValidIssuer": "dotnet-user-jwts" - } - } - }, \ No newline at end of file +"SiteSettings": { + "AdminEmail": "example@test.com", + "AdminPassword": "administrator" +}, + +"JwtTokenSettings": { + "ValidIssuer": "ExampleIssuer", + "ValidAudience": "ExampleAudience", + "SymmetricSecurityKey": "v89h3bh89vh9ve8hc89nv98nn899cnccn998ev80vi809jberh89b", + "JwtRegisteredClaimNamesSub": "rbveer3h535nn3n35nyny5umbbt" +}, \ No newline at end of file From 7056ee042194bdaf6e50644d130c36252db09e0a Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 22 Aug 2025 13:59:50 +0200 Subject: [PATCH 07/16] feat: Dtos, partial repo & customer endpoint --- .../DTOs/Customer/CustomerDto.cs | 12 ++++ .../DTOs/Customer/CustomerPostDto.cs | 9 +++ .../DTOs/Customer/CustomerPutDto.cs | 9 +++ .../DTOs/Movie/MovieDto.cs | 16 +++++ .../DTOs/Movie/MoviePostDto.cs | 6 ++ .../DTOs/Movie/MoviePutDto.cs | 6 ++ .../DTOs/Screening/ScreeningDto.cs | 12 ++++ .../DTOs/Screening/ScreeningPostDto.cs | 9 +++ .../DTOs/Ticket/TicketDto.cs | 7 ++ .../DTOs/Ticket/TicketPostDto.cs | 7 ++ .../Endpoints/CustomerEndpoint.cs | 56 +++++++++++++++- .../Endpoints/ScreeningEndpoint.cs | 6 -- .../Endpoints/TicketEndpoint.cs | 6 -- .../Factories/{Ticket.cs => TicketFactory.cs} | 2 +- .../api-cinema-challenge/Models/Customer.cs | 2 + .../api-cinema-challenge/Models/Movie.cs | 3 +- .../api-cinema-challenge/Models/Screening.cs | 2 + .../api-cinema-challenge/Models/Ticket.cs | 2 +- .../Repository/CustomerRepository.cs | 64 +++++++++++++++++++ .../Interfaces/ICustomerRepository.cs | 9 ++- .../Repository/Interfaces/IMovieRepository.cs | 9 ++- .../Interfaces/IScreeningRepository.cs | 7 +- .../Interfaces/ITicketRepository.cs | 6 +- .../api-cinema-challenge.csproj | 2 + 24 files changed, 248 insertions(+), 21 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPostDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPutDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningPostDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketPostDto.cs delete mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs delete mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs rename api-cinema-challenge/api-cinema-challenge/Factories/{Ticket.cs => TicketFactory.cs} (65%) create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerDto.cs new file mode 100644 index 00000000..ffea0115 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerDto.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.DTOs.Customer +{ + public class CustomerDto + { + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPostDto.cs new file mode 100644 index 00000000..d1927f50 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPostDto.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs.Customer +{ + public class CustomerPostDto + { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPutDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPutDto.cs new file mode 100644 index 00000000..ec0a4345 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPutDto.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs.Customer +{ + public class CustomerPutDto + { + public string? Name { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs new file mode 100644 index 00000000..d68dcaf1 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs @@ -0,0 +1,16 @@ +using api_cinema_challenge.DTOs.Screening; + +namespace api_cinema_challenge.DTOs.Movie +{ + public class MovieDto + { + public int Id { get; set; } + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + public ICollection Screenings { get; set; } = new List(); + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs new file mode 100644 index 00000000..86cca8a2 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.DTOs.Movie +{ + public class MoviePostDto + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs new file mode 100644 index 00000000..39d25f97 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.DTOs.Movie +{ + public class MoviePutDto + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningDto.cs new file mode 100644 index 00000000..82512795 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningDto.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.DTOs.Screening +{ + public class ScreeningDto + { + public int Id { get; set; } + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningPostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningPostDto.cs new file mode 100644 index 00000000..2fd735e3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningPostDto.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs.Screening +{ + public class ScreeningPostDto + { + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs new file mode 100644 index 00000000..b1819f24 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs @@ -0,0 +1,7 @@ +namespace api_cinema_challenge.DTOs.Ticket +{ + public class TicketDto + { + public int SeatNumber { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketPostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketPostDto.cs new file mode 100644 index 00000000..67c75452 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketPostDto.cs @@ -0,0 +1,7 @@ +namespace api_cinema_challenge.DTOs.Ticket +{ + public class TicketPostDto + { + public int NumSeats { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs index df19497e..9515bb03 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -1,6 +1,58 @@ -namespace api_cinema_challenge.Endpoints +using api_cinema_challenge.DTOs.Customer; +using api_cinema_challenge.Factories; +using api_cinema_challenge.Repository.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints { - public class CustomerEndpoint + public static class CustomerEndpoint { + public static void ConfigureCustomerEndpoint(this WebApplication app) + { + string groupName = "customer"; + string contentType = "application/json"; + + var customerGroup = app.MapGroup(groupName); + + customerGroup.MapGet("/", GetAllCustomers); + customerGroup.MapPost("/", CreateCustomer).Accepts(contentType); + customerGroup.MapPut("/{customerId}", UpdateCustomer).Accepts(contentType); + customerGroup.MapDelete("/{customerId}", DeleteCustomer); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetAllCustomers(ICustomerRepository repository) + { + var customers = await repository.GetAllAsync(); + + List dtos = new List(); + foreach (var customer in customers) + { + dtos.Add(CustomerFactory.DtoFromCustomer(customer)); + } + + return TypedResults.Ok(dtos); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + private static async Task CreateCustomer(ICustomerRepository repository, HttpRequest request) + { + + return TypedResults.Created(); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + private static async Task UpdateCustomer(ICustomerRepository repository, HttpRequest request) + { + + return TypedResults.Created(); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task DeleteCustomer(ICustomerRepository repository, HttpRequest request) + { + + return TypedResults.Created(); + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs deleted file mode 100644 index 4c4f95d9..00000000 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace api_cinema_challenge.Endpoints -{ - public class ScreeningEndpoint - { - } -} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs deleted file mode 100644 index 8b047e82..00000000 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace api_cinema_challenge.Endpoints -{ - public class TicketEndpoint - { - } -} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Factories/TicketFactory.cs similarity index 65% rename from api-cinema-challenge/api-cinema-challenge/Factories/Ticket.cs rename to api-cinema-challenge/api-cinema-challenge/Factories/TicketFactory.cs index d1e041ee..3826b570 100644 --- a/api-cinema-challenge/api-cinema-challenge/Factories/Ticket.cs +++ b/api-cinema-challenge/api-cinema-challenge/Factories/TicketFactory.cs @@ -1,6 +1,6 @@ namespace api_cinema_challenge.Factories { - public class Ticket + public class TicketFactory { } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs index 9620123b..7f73d7a6 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -7,5 +7,7 @@ public class Customer public string Email { get; set; } public string Phone { get; set; } public ICollection Tickets { get; set; } = new List(); + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs index 4c0b02a9..43f5e4ac 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -8,6 +8,7 @@ public class Movie public string Description { get; set; } public int RuntimeMins { get; set; } public ICollection Screenings { get; set; } = new List(); - + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs index ae10ac90..5f12a8e7 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -9,5 +9,7 @@ public class Screening public int Capacity { get; set; } public DateTime StartsAt { get; set; } public ICollection Tickets { get; set; } = new List(); + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs index 8199e608..e410f9c7 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -7,6 +7,6 @@ public class Ticket public Customer Customer { get; set; } public int ScreeningId { get; set; } public Screening Screening { get; set; } - public int NumSeats { get; set; } + public int SeatNumber { get; set; } } } 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..f79ab57f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs @@ -0,0 +1,64 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository +{ + public class CustomerRepository : ICustomerRepository + { + private CinemaContext _db; + + public CustomerRepository(CinemaContext db) + { + _db = db; + } + + public async Task CreateCustomer(Customer customer) + { + await _db.Customers.AddAsync(customer); + await _db.SaveChangesAsync(); + + return customer; + } + + public async Task DeleteCustomer(int id) + { + var exists = await _db.Customers.AnyAsync(x => x.Id == id); + if (!exists) + { + return null; + } + + var entity = await _db.Customers.FirstAsync(x => x.Id == id); + _db.Customers.Remove(entity); + await _db.SaveChangesAsync(); + + return entity; + } + + public async Task> GetAllAsync() + { + return await _db.Customers.ToListAsync(); + } + + public async Task GetByIdAsync(int id) + { + var entity = await _db.Customers.Where(c => c.Id == id).FirstOrDefaultAsync(); + if (entity is null) + { + return null; + } + + return entity; + } + + public async Task UpdateCustomer(Customer customer) + { + _db.Customers.Update(customer); + await _db.SaveChangesAsync(); + + return customer; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs index 3e5b4167..016b4b85 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs @@ -1,6 +1,13 @@ -namespace api_cinema_challenge.Repository.Interfaces +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository.Interfaces { public interface ICustomerRepository { + public Task GetByIdAsync(int id); + public Task> GetAllAsync(); + public Task CreateCustomer(Customer customer); + public Task UpdateCustomer(Customer customer); + public Task DeleteCustomer(int id); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs index 36fce701..767f7e55 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs @@ -1,6 +1,13 @@ -namespace api_cinema_challenge.Repository.Interfaces +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository.Interfaces { public interface IMovieRepository { + public Task GetByIdAsync(int id); + public Task> GetAllAsync(); + public Task CreateMovie(Movie movie); + public Task UpdateMovie(Movie customer); + public Task DeleteMovie(int id); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs index d1fe1399..4bf6f93e 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs @@ -1,6 +1,11 @@ -namespace api_cinema_challenge.Repository.Interfaces +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository.Interfaces { public interface IScreeningRepository { + public Task GetByIdAsync(int id); + public Task> GetAllAsync(); + public Task CreateScreening(Screening screening); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs index 2ac9acf3..d979ae32 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs @@ -1,6 +1,10 @@ -namespace api_cinema_challenge.Repository.Interfaces +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository.Interfaces { public interface ITicketRepository { + public Task GetByIdAsync(int customerId, int screeningId); + public Task CreateScreening(Screening screening); } } 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 4e306eae..635ea382 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -16,6 +16,7 @@ + @@ -25,6 +26,7 @@ + From 361ec4bc21178aa70555f533c109a92a7efef167 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 22 Aug 2025 15:09:24 +0200 Subject: [PATCH 08/16] feat: model factories --- .../DTOs/Movie/MovieDto.cs | 1 - .../DTOs/Movie/MoviePostDto.cs | 9 +++- .../DTOs/Movie/MoviePutDto.cs | 4 ++ .../DTOs/Ticket/TicketDto.cs | 5 +- .../Factories/CustomerFactory.cs | 44 +++++++++++++++++- .../Factories/MovieFactory.cs | 46 ++++++++++++++++++- .../Factories/ScreeningFactory.cs | 34 +++++++++++++- .../Factories/TicketFactory.cs | 32 ++++++++++++- .../api-cinema-challenge/Models/Ticket.cs | 4 +- 9 files changed, 167 insertions(+), 12 deletions(-) diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs index d68dcaf1..ee8d83d2 100644 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs @@ -9,7 +9,6 @@ public class MovieDto public string Rating { get; set; } public string Description { get; set; } public int RuntimeMins { get; set; } - public ICollection Screenings { get; set; } = new List(); public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs index 86cca8a2..35ab1869 100644 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs @@ -1,6 +1,13 @@ -namespace api_cinema_challenge.DTOs.Movie +using api_cinema_challenge.DTOs.Screening; + +namespace api_cinema_challenge.DTOs.Movie { public class MoviePostDto { + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + public ICollection Screenings { get; set; } = new List(); } } diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs index 39d25f97..c22c0c38 100644 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs @@ -2,5 +2,9 @@ { public class MoviePutDto { + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs index b1819f24..74629d0e 100644 --- a/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs @@ -2,6 +2,9 @@ { public class TicketDto { - public int SeatNumber { get; set; } + public int Id { get; set; } + public int NumSeats { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs index eba9b68f..67d7e231 100644 --- a/api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs +++ b/api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs @@ -1,6 +1,46 @@ -namespace api_cinema_challenge.Factories +using api_cinema_challenge.DTOs.Customer; +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Factories { - public class CustomerFactory + public static class CustomerFactory { + public static CustomerDto DtoFromCustomer(Customer customer) + { + var dto = new CustomerDto(); + + dto.Id = customer.Id; + dto.Name = customer.Name; + dto.Email = customer.Email; + dto.Phone = customer.Phone; + dto.CreatedAt = customer.CreatedAt; + dto.UpdatedAt = customer.UpdatedAt; + + return dto; + } + + public static Customer CustomerFromPostDto(CustomerPostDto dto) + { + var customer = new Customer(); + + customer.Name = dto.Name; + customer.Email = dto.Email; + customer.Phone = dto.Phone; + customer.CreatedAt = DateTime.UtcNow; + customer.UpdatedAt = DateTime.UtcNow; + + return customer; + } + + public static Customer CustomerFromPutDto(CustomerPutDto dto, Customer oldCustomer) + { + var updated = oldCustomer; + + if (dto.Name is not null) updated.Name = dto.Name; + if (dto.Email is not null) updated.Email = dto.Email; + if (dto.Phone is not null) updated.Phone = dto.Phone; + + return updated; + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs index 02d44b92..53eed04f 100644 --- a/api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs +++ b/api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs @@ -1,6 +1,48 @@ -namespace api_cinema_challenge.Factories +using api_cinema_challenge.DTOs.Movie; +using api_cinema_challenge.Models; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; + +namespace api_cinema_challenge.Factories { - public class MovieFactory + public static class MovieFactory { + public static MovieDto DtoFromMovie(Movie movie) + { + var dto = new MovieDto(); + + dto.Id = movie.Id; + dto.Title = movie.Title; + dto.Rating = movie.Rating; + dto.Description = movie.Description; + dto.RuntimeMins = movie.RuntimeMins; + dto.CreatedAt = movie.CreatedAt; + dto.UpdatedAt = movie.UpdatedAt; + + return dto; + } + + public static Movie MovieFromPostDto(MoviePostDto dto) + { + var movie = new Movie(); + + movie.Title = dto.Title; + movie.Rating = dto.Rating; + movie.Description = dto.Description; + movie.RuntimeMins = dto.RuntimeMins; + + return movie; + } + + public static Movie MovieFromPutDto(MoviePutDto dto, Movie oldMovie) + { + var updated = oldMovie; + + if (dto.Title is not null) updated.Title = dto.Title; + if (dto.Rating is not null) updated.Rating = dto.Rating; + if (dto.Description is not null) updated.Description = dto.Description; + if (dto.RuntimeMins != 0) updated.RuntimeMins = dto.RuntimeMins; + + return updated; + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs index 08891939..8eaf28b9 100644 --- a/api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs +++ b/api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs @@ -1,6 +1,36 @@ -namespace api_cinema_challenge.Factories +using api_cinema_challenge.DTOs.Screening; +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Factories { - public class ScreeningFactory + public static class ScreeningFactory { + public static Screening ScreeningFromPostDto(ScreeningPostDto dtp, int movieId) + { + var screening = new Screening(); + + screening.MovieId = movieId; + screening.ScreenNumber = dtp.ScreenNumber; + screening.Capacity = dtp.Capacity; + screening.StartsAt = dtp.StartsAt; + screening.CreatedAt = DateTime.UtcNow; + screening.UpdatedAt = DateTime.UtcNow; + + return screening; + } + + public static ScreeningDto DtoFromScreening(Screening screening) + { + var dto = new ScreeningDto(); + + dto.Id = screening.Id; + dto.ScreenNumber = screening.ScreenNumber; + dto.Capacity = screening.Capacity; + dto.StartsAt = screening.StartsAt; + dto.CreatedAt = DateTime.UtcNow; + dto.UpdatedAt = DateTime.UtcNow; + + return dto; + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/TicketFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/TicketFactory.cs index 3826b570..2f4f0617 100644 --- a/api-cinema-challenge/api-cinema-challenge/Factories/TicketFactory.cs +++ b/api-cinema-challenge/api-cinema-challenge/Factories/TicketFactory.cs @@ -1,6 +1,34 @@ -namespace api_cinema_challenge.Factories +using api_cinema_challenge.DTOs.Ticket; +using api_cinema_challenge.Models; +using Microsoft.AspNetCore.StaticAssets; + +namespace api_cinema_challenge.Factories { - public class TicketFactory + public static class TicketFactory { + public static Ticket TicketFromPostDto(TicketPostDto dto, int customerId, int screeningId) + { + var ticket = new Ticket(); + + ticket.CustomerId = customerId; + ticket.ScreeningId = screeningId; + ticket.NumSeats = dto.NumSeats; + ticket.CreatedAt = DateTime.UtcNow; + ticket.UpdatedAt = DateTime.UtcNow; + + return ticket; + } + + public static TicketDto DtoFromTicket(Ticket ticket) + { + var dto = new TicketDto(); + + dto.Id = ticket.Id; + dto.NumSeats = ticket.NumSeats; + dto.CreatedAt = ticket.CreatedAt; + dto.UpdatedAt = ticket.UpdatedAt; + + return dto; + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs index e410f9c7..9e93413d 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -7,6 +7,8 @@ public class Ticket public Customer Customer { get; set; } public int ScreeningId { get; set; } public Screening Screening { get; set; } - public int SeatNumber { get; set; } + public int NumSeats { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } } } From 94bc3c485ffbb22ba06d51e549886c189097e14a Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 22 Aug 2025 15:45:07 +0200 Subject: [PATCH 09/16] feat: partial customer endpoint --- .../Endpoints/CustomerEndpoint.cs | 33 ++++++++++++++++++- .../Factories/ResponseFactory.cs | 6 ++++ .../Repository/CustomerRepository.cs | 3 ++ .../api-cinema-challenge/Utils/Utility.cs | 26 +++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Utils/Utility.cs diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs index 9515bb03..9a33f811 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -1,6 +1,7 @@ using api_cinema_challenge.DTOs.Customer; using api_cinema_challenge.Factories; using api_cinema_challenge.Repository.Interfaces; +using api_cinema_challenge.Utils; using Microsoft.AspNetCore.Mvc; namespace api_cinema_challenge.Endpoints @@ -37,13 +38,43 @@ private static async Task GetAllCustomers(ICustomerRepository repositor [ProducesResponseType(StatusCodes.Status201Created)] private static async Task CreateCustomer(ICustomerRepository repository, HttpRequest request) { + CustomerPostDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(); + } + + var added = await repository.CreateCustomer(CustomerFactory.CustomerFromPostDto(inDto)); + if (added is null) + { + return TypedResults.Conflict(); + } + var outDto = CustomerFactory.DtoFromCustomer(added); return TypedResults.Created(); } [ProducesResponseType(StatusCodes.Status201Created)] - private static async Task UpdateCustomer(ICustomerRepository repository, HttpRequest request) + private static async Task UpdateCustomer(ICustomerRepository repository, HttpRequest request, int id) { + CustomerPutDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(); + } + + var entity = await repository.GetByIdAsync(id); + if (entity is null) + { + return TypedResults.NotFound(); + } + + var updated = await repository.UpdateCustomer(CustomerFactory.CustomerFromPutDto(inDto, entity)); + if (updated is null) + { + return TypedResults.Conflict(); + } + return TypedResults.Created(); } diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs new file mode 100644 index 00000000..6ec3e47e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Factories +{ + public static class ResponseFactory + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs index f79ab57f..48cce0bb 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs @@ -16,6 +16,9 @@ public CustomerRepository(CinemaContext db) public async Task CreateCustomer(Customer customer) { + var exists = _db.Customers.Where(c => c.Id == customer.Id).Any(); + if (exists) return null; + await _db.Customers.AddAsync(customer); await _db.SaveChangesAsync(); diff --git a/api-cinema-challenge/api-cinema-challenge/Utils/Utility.cs b/api-cinema-challenge/api-cinema-challenge/Utils/Utility.cs new file mode 100644 index 00000000..66eed50f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Utils/Utility.cs @@ -0,0 +1,26 @@ +using System.Text.Json; + +namespace api_cinema_challenge.Utils +{ + public static class Utility + { + public static async Task ValidateFromRequest(HttpRequest request) + { + T? entity; + try + { + entity = await request.ReadFromJsonAsync(); + } + catch (JsonException ex) + { + return default; + } + catch (Exception ex) + { + return default; + } + + return entity; + } + } +} From 6de59651fe3d8e629bda8f9da3907d3834ab6907 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Mon, 25 Aug 2025 09:31:23 +0200 Subject: [PATCH 10/16] feat: customer endpoint --- .../Data/CinemaContext.cs | 14 ++ .../Endpoints/CustomerEndpoint.cs | 64 +++++- .../api-cinema-challenge/Enums/RoleEnum.cs | 8 + .../Factories/ResponseFactory.cs | 9 + .../20250825061647_First.Designer.cs | 205 ++++++++++++++++++ .../Migrations/20250825061647_First.cs | 135 ++++++++++++ .../Migrations/CinemaContextModelSnapshot.cs | 202 +++++++++++++++++ .../Models/ApplicationUser.cs | 10 + .../api-cinema-challenge/Program.cs | 44 +++- .../Interfaces/ITicketRepository.cs | 4 +- .../Repository/TicketRepository.cs | 45 ++++ .../api-cinema-challenge.csproj | 4 - 12 files changed, 726 insertions(+), 18 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Enums/RoleEnum.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index 7acbfcad..50dcb3b7 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -13,6 +13,20 @@ public CinemaContext(DbContextOptions options) : base(options) protected override void OnModelCreating(ModelBuilder modelBuilder) { // relations + modelBuilder.Entity() + .HasOne(t => t.Customer) + .WithMany(c => c.Tickets) + .HasForeignKey(t => t.CustomerId); + + modelBuilder.Entity() + .HasOne(t => t.Screening) + .WithMany(s => s.Tickets) + .HasForeignKey(t => t.ScreeningId); + + modelBuilder.Entity() + .HasOne(s => s.Movie) + .WithMany(m => m.Screenings) + .HasForeignKey(s => s.MovieId); // seeder } diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs index 9a33f811..234a7331 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -1,4 +1,5 @@ using api_cinema_challenge.DTOs.Customer; +using api_cinema_challenge.DTOs.Ticket; using api_cinema_challenge.Factories; using api_cinema_challenge.Repository.Interfaces; using api_cinema_challenge.Utils; @@ -10,7 +11,7 @@ public static class CustomerEndpoint { public static void ConfigureCustomerEndpoint(this WebApplication app) { - string groupName = "customer"; + string groupName = "customers"; string contentType = "application/json"; var customerGroup = app.MapGroup(groupName); @@ -19,6 +20,9 @@ public static void ConfigureCustomerEndpoint(this WebApplication app) customerGroup.MapPost("/", CreateCustomer).Accepts(contentType); customerGroup.MapPut("/{customerId}", UpdateCustomer).Accepts(contentType); customerGroup.MapDelete("/{customerId}", DeleteCustomer); + + customerGroup.MapPost("/{customerId}/screenings/{screeningId}", BookTicket).Accepts(contentType); + customerGroup.MapGet("/{customerId}/screenings/{screeningId}", GetTickets); } [ProducesResponseType(StatusCodes.Status200OK)] @@ -32,7 +36,7 @@ private static async Task GetAllCustomers(ICustomerRepository repositor dtos.Add(CustomerFactory.DtoFromCustomer(customer)); } - return TypedResults.Ok(dtos); + return TypedResults.Ok(new { Status = "success", Data = dtos}); } [ProducesResponseType(StatusCodes.Status201Created)] @@ -41,17 +45,21 @@ private static async Task CreateCustomer(ICustomerRepository repository CustomerPostDto inDto = await Utility.ValidateFromRequest(request); if (inDto is null) { - return TypedResults.BadRequest(); + return TypedResults.BadRequest( new { status = "failure"} ); } var added = await repository.CreateCustomer(CustomerFactory.CustomerFromPostDto(inDto)); if (added is null) { - return TypedResults.Conflict(); + return TypedResults.BadRequest(new { status = "failure" }); } var outDto = CustomerFactory.DtoFromCustomer(added); - return TypedResults.Created(); + + // TODO move to other class + var url = $"{request.Scheme}://{request.Host}{request.Path}/{outDto.Id}"; + + return TypedResults.Created(url, new {status = "success", data = outDto}); } [ProducesResponseType(StatusCodes.Status201Created)] @@ -60,19 +68,19 @@ private static async Task UpdateCustomer(ICustomerRepository repository CustomerPutDto inDto = await Utility.ValidateFromRequest(request); if (inDto is null) { - return TypedResults.BadRequest(); + return TypedResults.BadRequest(new { status = "failure" }); } var entity = await repository.GetByIdAsync(id); if (entity is null) { - return TypedResults.NotFound(); + return TypedResults.NotFound(new { status = "failure" }); } var updated = await repository.UpdateCustomer(CustomerFactory.CustomerFromPutDto(inDto, entity)); if (updated is null) { - return TypedResults.Conflict(); + return TypedResults.BadRequest(new { status = "failure" }); } @@ -80,9 +88,47 @@ private static async Task UpdateCustomer(ICustomerRepository repository } [ProducesResponseType(StatusCodes.Status200OK)] - private static async Task DeleteCustomer(ICustomerRepository repository, HttpRequest request) + private static async Task DeleteCustomer(ICustomerRepository repository, int customerId) { + var customer = await repository.DeleteCustomer(customerId); + if (customer is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + + var dto = CustomerFactory.DtoFromCustomer(customer); + return TypedResults.Ok( new { status = "success", data = dto}); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + private static async Task BookTicket(ITicketRepository ticketRepository, HttpRequest request, int customerId, int screeningId) + { + TicketPostDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var ticket = await ticketRepository.CreateTicket(TicketFactory.TicketFromPostDto(inDto, customerId, screeningId)); + if (ticket is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var dto = TicketFactory.DtoFromTicket(ticket); + return TypedResults.Created(); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetTickets(ITicketRepository ticketRepository, int customerId, int screeningId) + { + var ticket = await ticketRepository.GetByIdAsync(customerId, screeningId); + if (ticket is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + var dto = TicketFactory.DtoFromTicket(ticket); return TypedResults.Created(); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Enums/RoleEnum.cs b/api-cinema-challenge/api-cinema-challenge/Enums/RoleEnum.cs new file mode 100644 index 00000000..1daf8615 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Enums/RoleEnum.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.Enums +{ + public enum RoleEnum + { + User, + Admin + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs index 6ec3e47e..0934372b 100644 --- a/api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs +++ b/api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs @@ -2,5 +2,14 @@ { public static class ResponseFactory { + public static Object Failure() + { + return new { status = "failure"}; + } + + public static Object Success() + { + return new { status = "success" }; + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.Designer.cs new file mode 100644 index 00000000..69180b3a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.Designer.cs @@ -0,0 +1,205 @@ +// +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("20250825061647_First")] + partial class First + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.HasOne("api_cinema_challenge.Models.Movie", "Movie") + .WithMany("Screenings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.HasOne("api_cinema_challenge.Models.Customer", "Customer") + .WithMany("Tickets") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany("Tickets") + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.cs new file mode 100644 index 00000000..49c016b2 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.cs @@ -0,0 +1,135 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class First : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Movies", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "text", nullable: false), + Rating = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + RuntimeMins = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Movies", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Screenings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MovieId = table.Column(type: "integer", nullable: false), + ScreenNumber = table.Column(type: "integer", nullable: false), + Capacity = table.Column(type: "integer", nullable: false), + StartsAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Screenings", x => x.Id); + table.ForeignKey( + name: "FK_Screenings_Movies_MovieId", + column: x => x.MovieId, + principalTable: "Movies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CustomerId = table.Column(type: "integer", nullable: false), + ScreeningId = table.Column(type: "integer", nullable: false), + NumSeats = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => x.Id); + table.ForeignKey( + name: "FK_Tickets_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Tickets_Screenings_ScreeningId", + column: x => x.ScreeningId, + principalTable: "Screenings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Screenings_MovieId", + table: "Screenings", + column: "MovieId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_CustomerId", + table: "Tickets", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_ScreeningId", + table: "Tickets", + column: "ScreeningId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Tickets"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Screenings"); + + migrationBuilder.DropTable( + name: "Movies"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs new file mode 100644 index 00000000..a1897d11 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,202 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + partial class CinemaContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.HasOne("api_cinema_challenge.Models.Movie", "Movie") + .WithMany("Screenings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.HasOne("api_cinema_challenge.Models.Customer", "Customer") + .WithMany("Tickets") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany("Tickets") + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs new file mode 100644 index 00000000..c3af11d2 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs @@ -0,0 +1,10 @@ +using api_cinema_challenge.Enums; +using System.Data; + +namespace api_cinema_challenge.Models +{ + public class ApplicationUser + { + public RoleEnum Role { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index ab775bb5..44d28265 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,5 +1,10 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using api_cinema_challenge.Repository.Interfaces; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -10,12 +15,14 @@ using System.Diagnostics; using System.Diagnostics; using System.Text; +using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddOpenApi(); +/* builder.Services.AddSwaggerGen(option => { option.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); @@ -44,6 +51,7 @@ }); }); +*/ // Dependency injection //builder.Services.AddDbContext(); builder.Services.AddDbContext(options => { @@ -53,6 +61,34 @@ options.EnableSensitiveDataLogging(); }); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Support string to enum conversions +builder.Services.AddControllers().AddJsonOptions(opt => +{ + opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); + +/* +// Specify identity requirements +// Must be added before .AddAuthentication otherwise a 404 is thrown on authorized endpoints +builder.Services + .AddIdentity(options => + { + options.SignIn.RequireConfirmedAccount = false; + options.User.RequireUniqueEmail = true; + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + }) + .AddRoles(); + //.AddEntityFrameworkStores(); // doesnt work + +*/ + +/* // security // These will eventually be moved to a secrets file, but for alpha development appsettings is fine var validIssuer = builder.Configuration.GetValue("JwtTokenSettings:ValidIssuer"); @@ -81,7 +117,8 @@ ), }; }); -//builder.Services.AddAuthorization(); + +*/ var app = builder.Build(); @@ -98,9 +135,10 @@ app.UseHttpsRedirection(); -app.UseAuthentication(); -app.UseAuthorization(); +//app.UseAuthentication(); +//app.UseAuthorization(); // endpoints configuration +app.ConfigureCustomerEndpoint(); app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs index d979ae32..833b873e 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs @@ -4,7 +4,7 @@ namespace api_cinema_challenge.Repository.Interfaces { public interface ITicketRepository { - public Task GetByIdAsync(int customerId, int screeningId); - public Task CreateScreening(Screening screening); + public Task GetByIdAsync(int customerId, int screeningId); + public Task CreateTicket(Ticket ticket); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs new file mode 100644 index 00000000..ee89fbb2 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs @@ -0,0 +1,45 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository +{ + public class TicketRepository : ITicketRepository + { + private CinemaContext _db; + + public TicketRepository(CinemaContext db) + { + _db = db; + } + + public async Task CreateTicket(Ticket ticket) + { + var exists = await _db.Tickets + .Where(t => t.CustomerId == ticket.CustomerId) + .Where(t => t.ScreeningId == ticket.ScreeningId) + .AnyAsync(); + + if (exists) return null; + + await _db.Tickets.AddAsync(ticket); + await _db.SaveChangesAsync(); + + return ticket; + } + + public async Task GetByIdAsync(int customerId, int screeningId) + { + var ticket = await _db.Tickets + .Where(t => t.CustomerId == customerId) + .Where(t => t.ScreeningId==screeningId) + .FirstOrDefaultAsync(); + + if (ticket is null) + return null; + + return ticket; + } + } +} 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 635ea382..751eb8b0 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -30,8 +30,4 @@ - - - - From f485cf7e8fff8ae2aab12f5418009e9ec1b4a59d Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Mon, 25 Aug 2025 10:26:59 +0200 Subject: [PATCH 11/16] feat: movie endpoints --- .../Controllers/UserController.cs | 6 + .../Endpoints/CustomerEndpoint.cs | 17 ++- .../Endpoints/MovieEndpoint.cs | 141 +++++++++++++++++- .../Endpoints/UserEndpoint.cs | 6 + .../Models/ApplicationUser.cs | 3 +- .../api-cinema-challenge/Program.cs | 2 + .../Interfaces/IScreeningRepository.cs | 3 +- .../Interfaces/ITicketRepository.cs | 2 +- .../Repository/MovieRepository.cs | 41 +++++ .../Repository/ScreeningRepository.cs | 48 ++++++ .../Repository/TicketRepository.cs | 18 ++- 11 files changed, 270 insertions(+), 17 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/UserEndpoint.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 diff --git a/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs new file mode 100644 index 00000000..4625ee6b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Controllers +{ + public class UserController + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs index 234a7331..fe5ff2b0 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -1,6 +1,7 @@ using api_cinema_challenge.DTOs.Customer; using api_cinema_challenge.DTOs.Ticket; using api_cinema_challenge.Factories; +using api_cinema_challenge.Models; using api_cinema_challenge.Repository.Interfaces; using api_cinema_challenge.Utils; using Microsoft.AspNetCore.Mvc; @@ -120,16 +121,22 @@ private static async Task BookTicket(ITicketRepository ticketRepository } [ProducesResponseType(StatusCodes.Status200OK)] - private static async Task GetTickets(ITicketRepository ticketRepository, int customerId, int screeningId) + private static async Task GetTickets(ITicketRepository ticketRepository, HttpRequest request, int customerId, int screeningId) { - var ticket = await ticketRepository.GetByIdAsync(customerId, screeningId); - if (ticket is null) + var tickets = await ticketRepository.GetByIdAsync(customerId, screeningId); + if (tickets is null) { return TypedResults.NotFound(new { status = "failure" }); } - var dto = TicketFactory.DtoFromTicket(ticket); - return TypedResults.Created(); + List dtos = new(); + foreach (var ticket in tickets) + { + dtos.Add(TicketFactory.DtoFromTicket(ticket)); + } + // TODO fix url + var url = $"{request.Scheme}://{request.Host}{request.Path}/"; + return TypedResults.Created(url, new { status = "success", data = dtos}); } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs index 897b8e5c..f9ea09a0 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -1,6 +1,143 @@ -namespace api_cinema_challenge.Endpoints +using api_cinema_challenge.DTOs.Customer; +using api_cinema_challenge.DTOs.Movie; +using api_cinema_challenge.DTOs.Screening; +using api_cinema_challenge.DTOs.Ticket; +using api_cinema_challenge.Factories; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; +using api_cinema_challenge.Utils; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints { - public class MovieEndpoint + public static class MovieEndpoint { + public static void ConfigureCustomerEndpoint(this WebApplication app) + { + const string groupName = "movies"; + const string contentType = "application/json"; + + var moviesGroup = app.MapGroup(groupName); + + moviesGroup.MapGet("/", GetAllMovies); + moviesGroup.MapPost("/", CreateMovie).Accepts(contentType); + moviesGroup.MapPut("/{movieId}", UpdateMovie).Accepts(contentType); + moviesGroup.MapDelete("/{movieId}", DeleteMovie); + + moviesGroup.MapPost("/{movieId}/screenings", CreateScreening).Accepts(contentType); + moviesGroup.MapGet("/{movieId}/screenings", GetScreenings); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetAllMovies(IMovieRepository repository) + { + var movie = await repository.GetAllAsync(); + + List dtos = new(); + foreach (var customer in movie) + { + dtos.Add(MovieFactory.DtoFromMovie(customer)); + } + + return TypedResults.Ok(new { Status = "success", Data = dtos }); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + private static async Task CreateMovie(IMovieRepository repository, HttpRequest request) + { + MoviePostDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var added = await repository.CreateMovie(MovieFactory.MovieFromPostDto(inDto)); + if (added is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var outDto = MovieFactory.DtoFromMovie(added); + + var url = $"{request.Scheme}://{request.Host}{request.Path}/{outDto.Id}"; + + return TypedResults.Created(url, new { status = "success", data = outDto }); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + private static async Task UpdateMovie(IMovieRepository repository, HttpRequest request, int movieId) + { + MoviePutDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var entity = await repository.GetByIdAsync(movieId); + if (entity is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + + var updated = await repository.UpdateMovie(MovieFactory.MovieFromPutDto(inDto, entity)); + if (updated is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + + return TypedResults.Created(); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task DeleteMovie(IMovieRepository repository, int movieId) + { + var movie = await repository.DeleteMovie(movieId); + if (movie is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + + var dto = MovieFactory.DtoFromMovie(movie); + return TypedResults.Ok(new { status = "success", data = dto }); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + private static async Task CreateScreening(IScreeningRepository repository, HttpRequest request, int movieId) + { + ScreeningPostDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var ticket = await repository.CreateScreening(ScreeningFactory.ScreeningFromPostDto(inDto, movieId)); + if (ticket is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var dto = ScreeningFactory.DtoFromScreening(ticket); + return TypedResults.Created(); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetScreenings(IScreeningRepository repository, HttpRequest request, int movieId) + { + var screenings = await repository.GetByIdAsync(movieId); + if (screenings is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + + List dtos = new(); + foreach (var screening in screenings) + { + dtos.Add(ScreeningFactory.DtoFromScreening(screening)); + } + // TODO fix url + var url = $"{request.Scheme}://{request.Host}{request.Path}/"; + return TypedResults.Created(url, new { status = "success", data = dtos }); + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/UserEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/UserEndpoint.cs new file mode 100644 index 00000000..8e36ead7 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/UserEndpoint.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Endpoints +{ + public class UserEndpoint + { + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs index c3af11d2..4120d5ab 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs @@ -1,9 +1,10 @@ using api_cinema_challenge.Enums; +using Microsoft.AspNetCore.Identity; using System.Data; namespace api_cinema_challenge.Models { - public class ApplicationUser + public class ApplicationUser : IdentityUser { public RoleEnum Role { get; set; } } diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 44d28265..1ed4e43c 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -63,6 +63,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // Support string to enum conversions builder.Services.AddControllers().AddJsonOptions(opt => diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs index 4bf6f93e..78f8009e 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs @@ -4,8 +4,7 @@ namespace api_cinema_challenge.Repository.Interfaces { public interface IScreeningRepository { - public Task GetByIdAsync(int id); - public Task> GetAllAsync(); + public Task> GetByIdAsync(int movieId); public Task CreateScreening(Screening screening); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs index 833b873e..49483f04 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs @@ -4,7 +4,7 @@ namespace api_cinema_challenge.Repository.Interfaces { public interface ITicketRepository { - public Task GetByIdAsync(int customerId, int screeningId); + public Task> GetByIdAsync(int customerId, int screeningId); public Task CreateTicket(Ticket ticket); } } 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..24d7fa30 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs @@ -0,0 +1,41 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; + +namespace api_cinema_challenge.Repository +{ + public class MovieRepository : IMovieRepository + { + private CinemaContext _db; + + public MovieRepository(CinemaContext db) + { + _db = db; + } + + public Task CreateMovie(Movie movie) + { + throw new NotImplementedException(); + } + + public Task DeleteMovie(int id) + { + throw new NotImplementedException(); + } + + public Task> GetAllAsync() + { + throw new NotImplementedException(); + } + + public Task GetByIdAsync(int id) + { + throw new NotImplementedException(); + } + + public Task UpdateMovie(Movie customer) + { + throw new NotImplementedException(); + } + } +} 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..1a577fbe --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/ScreeningRepository.cs @@ -0,0 +1,48 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; +using Microsoft.EntityFrameworkCore; +using System.Net.Sockets; + +namespace api_cinema_challenge.Repository +{ + public class ScreeningRepository : IScreeningRepository + { + private CinemaContext _db; + + public ScreeningRepository(CinemaContext db) + { + _db = db; + } + + public async Task CreateScreening(Screening screening) + { + var exists = await _db.Screenings + .Where(s => s.Id == screening.Id) + .AnyAsync(); + + if (exists) return null; + + await _db.Screenings.AddAsync(screening); + await _db.SaveChangesAsync(); + + return screening; + } + + public async Task> GetByIdAsync(int movieId) + { + bool exists = await _db.Screenings + .Where(s => s.MovieId == movieId) + .AnyAsync(); + + if (!exists) + return null; + + var screenings = await _db.Screenings + .Where(s => s.MovieId == movieId) + .ToListAsync(); + + return screenings; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs index ee89fbb2..a69c6d77 100644 --- a/api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs +++ b/api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs @@ -2,6 +2,7 @@ using api_cinema_challenge.Models; using api_cinema_challenge.Repository.Interfaces; using Microsoft.EntityFrameworkCore; +using System.Net.Sockets; namespace api_cinema_challenge.Repository { @@ -29,17 +30,22 @@ public async Task CreateTicket(Ticket ticket) return ticket; } - public async Task GetByIdAsync(int customerId, int screeningId) + public async Task> GetByIdAsync(int customerId, int screeningId) { - var ticket = await _db.Tickets + bool exists = await _db.Tickets .Where(t => t.CustomerId == customerId) - .Where(t => t.ScreeningId==screeningId) - .FirstOrDefaultAsync(); + .Where(t => t.ScreeningId == screeningId) + .AnyAsync(); - if (ticket is null) + if (!exists) return null; - return ticket; + var tickets = await _db.Tickets + .Where(t => t.CustomerId == customerId) + .Where(t => t.ScreeningId==screeningId) + .ToListAsync(); + + return tickets; } } } From d72afed2c386965ccc0e2152a7e18c977dd0576c Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Mon, 25 Aug 2025 11:01:00 +0200 Subject: [PATCH 12/16] feat: authentication setup --- .gitignore | 1 + .../Controllers/UserController.cs | 97 ++++++- .../DTOs/Auth/AuthRequest.cs | 15 ++ .../DTOs/Auth/AuthResponse.cs | 8 + .../DTOs/Auth/RegistrationRequest.cs | 22 ++ .../Data/CinemaContext.cs | 13 +- .../Endpoints/MovieEndpoint.cs | 2 +- .../Enums/{RoleEnum.cs => Role.cs} | 2 +- .../20250825061647_First.Designer.cs | 205 -------------- .../Migrations/20250825061647_First.cs | 135 ---------- .../Migrations/CinemaContextModelSnapshot.cs | 251 ++++++++++++++++++ .../Models/ApplicationUser.cs | 2 +- .../api-cinema-challenge/Program.cs | 86 +++--- .../Services/TokenService.cs | 82 ++++++ .../api-cinema-challenge.csproj | 8 +- 15 files changed, 536 insertions(+), 393 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthResponse.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/Auth/RegistrationRequest.cs rename api-cinema-challenge/api-cinema-challenge/Enums/{RoleEnum.cs => Role.cs} (77%) delete mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.Designer.cs delete mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs diff --git a/.gitignore b/.gitignore index cf332414..bfcb6924 100644 --- a/.gitignore +++ b/.gitignore @@ -368,3 +368,4 @@ FodyWeavers.xsd */**/bin/Release */**/obj/Debug */**/obj/Release +/api-cinema-challenge/api-cinema-challenge/Migrations diff --git a/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs index 4625ee6b..21cca1a0 100644 --- a/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs +++ b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs @@ -1,6 +1,99 @@ -namespace api_cinema_challenge.Controllers +using api_cinema_challenge.Data; +using api_cinema_challenge.DTOs.Auth; +using api_cinema_challenge.Enums; +using api_cinema_challenge.Models; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Data; + +namespace api_cinema_challenge.Controllers { - public class UserController + [ApiController] + [Route("/api/[controller]")] + public class UsersController : ControllerBase { + private readonly UserManager _userManager; + private readonly CinemaContext _context; + private readonly TokenService _tokenService; + + public UsersController(UserManager userManager, CinemaContext context, + TokenService tokenService, ILogger logger) + { + _userManager = userManager; + _context = context; + _tokenService = tokenService; + } + + + [HttpPost] + [Route("register")] + public async Task Register(RegistrationRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var result = await _userManager.CreateAsync( + new ApplicationUser { UserName = request.Username, Email = request.Email, Role = request.Role }, + request.Password! + ); + + if (result.Succeeded) + { + request.Password = ""; + return CreatedAtAction(nameof(Register), new { email = request.Email, role = Role.User }, request); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(error.Code, error.Description); + } + + return BadRequest(ModelState); + } + + + [HttpPost] + [Route("login")] + public async Task> Authenticate([FromBody] AuthRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var managedUser = await _userManager.FindByEmailAsync(request.Email!); + + if (managedUser == null) + { + return BadRequest("Bad credentials"); + } + + var isPasswordValid = await _userManager.CheckPasswordAsync(managedUser, request.Password!); + + if (!isPasswordValid) + { + return BadRequest("Bad credentials"); + } + + var userInDb = _context.Users.FirstOrDefault(u => u.Email == request.Email); + + if (userInDb is null) + { + return Unauthorized(); + } + + var accessToken = _tokenService.CreateToken(userInDb); + await _context.SaveChangesAsync(); + + return Ok(new AuthResponse + { + Username = userInDb.UserName, + Email = userInDb.Email, + Token = accessToken, + }); + } } } diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthRequest.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthRequest.cs new file mode 100644 index 00000000..a7fe06c3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthRequest.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.Data; + +namespace api_cinema_challenge.DTOs.Auth; + +public class AuthRequest +{ + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() + { + return true; + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthResponse.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthResponse.cs new file mode 100644 index 00000000..2507065e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthResponse.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.DTOs.Auth; + +public class AuthResponse +{ + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/RegistrationRequest.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/RegistrationRequest.cs new file mode 100644 index 00000000..266eff7d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/RegistrationRequest.cs @@ -0,0 +1,22 @@ +using api_cinema_challenge.Enums; +using System.ComponentModel.DataAnnotations; +using System.Data; + +namespace api_cinema_challenge.DTOs.Auth; + + + + +public class RegistrationRequest +{ + [Required] + public string? Email { get; set; } + + [Required] + public string? Username { get { return this.Email; } set { } } + + [Required] + public string? Password { get; set; } + + public Role Role { get; set; } = Role.User; +} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index 50dcb3b7..fab6efcb 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -2,9 +2,14 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using System.Security.Claims; + namespace api_cinema_challenge.Data { - public class CinemaContext : DbContext + // IdentityUserContext instead of Db in workshop + public class CinemaContext : IdentityDbContext { public CinemaContext(DbContextOptions options) : base(options) { @@ -12,6 +17,12 @@ public CinemaContext(DbContextOptions options) : base(options) protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); // ← This is crucial + // Optional: configure enum as string + modelBuilder.Entity() + .Property(u => u.Role) + .HasConversion(); + // relations modelBuilder.Entity() .HasOne(t => t.Customer) diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs index f9ea09a0..b774abac 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -12,7 +12,7 @@ namespace api_cinema_challenge.Endpoints { public static class MovieEndpoint { - public static void ConfigureCustomerEndpoint(this WebApplication app) + public static void ConfigureMovieEndpoint(this WebApplication app) { const string groupName = "movies"; const string contentType = "application/json"; diff --git a/api-cinema-challenge/api-cinema-challenge/Enums/RoleEnum.cs b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs similarity index 77% rename from api-cinema-challenge/api-cinema-challenge/Enums/RoleEnum.cs rename to api-cinema-challenge/api-cinema-challenge/Enums/Role.cs index 1daf8615..3240a249 100644 --- a/api-cinema-challenge/api-cinema-challenge/Enums/RoleEnum.cs +++ b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs @@ -1,6 +1,6 @@ namespace api_cinema_challenge.Enums { - public enum RoleEnum + public enum Role { User, Admin diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.Designer.cs deleted file mode 100644 index 69180b3a..00000000 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.Designer.cs +++ /dev/null @@ -1,205 +0,0 @@ -// -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("20250825061647_First")] - partial class First - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Customers"); - }); - - modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("Rating") - .IsRequired() - .HasColumnType("text"); - - b.Property("RuntimeMins") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.ToTable("Movies"); - }); - - modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Capacity") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("MovieId") - .HasColumnType("integer"); - - b.Property("ScreenNumber") - .HasColumnType("integer"); - - b.Property("StartsAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("MovieId"); - - b.ToTable("Screenings"); - }); - - modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CustomerId") - .HasColumnType("integer"); - - b.Property("NumSeats") - .HasColumnType("integer"); - - b.Property("ScreeningId") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ScreeningId"); - - b.ToTable("Tickets"); - }); - - modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => - { - b.HasOne("api_cinema_challenge.Models.Movie", "Movie") - .WithMany("Screenings") - .HasForeignKey("MovieId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Movie"); - }); - - modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => - { - b.HasOne("api_cinema_challenge.Models.Customer", "Customer") - .WithMany("Tickets") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("api_cinema_challenge.Models.Screening", "Screening") - .WithMany("Tickets") - .HasForeignKey("ScreeningId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("Screening"); - }); - - modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => - { - b.Navigation("Tickets"); - }); - - modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => - { - b.Navigation("Screenings"); - }); - - modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => - { - b.Navigation("Tickets"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.cs deleted file mode 100644 index 49c016b2..00000000 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825061647_First.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace api_cinema_challenge.Migrations -{ - /// - public partial class First : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Customers", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "text", nullable: false), - Email = table.Column(type: "text", nullable: false), - Phone = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Customers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Movies", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Title = table.Column(type: "text", nullable: false), - Rating = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), - RuntimeMins = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Movies", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Screenings", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - MovieId = table.Column(type: "integer", nullable: false), - ScreenNumber = table.Column(type: "integer", nullable: false), - Capacity = table.Column(type: "integer", nullable: false), - StartsAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Screenings", x => x.Id); - table.ForeignKey( - name: "FK_Screenings_Movies_MovieId", - column: x => x.MovieId, - principalTable: "Movies", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Tickets", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - CustomerId = table.Column(type: "integer", nullable: false), - ScreeningId = table.Column(type: "integer", nullable: false), - NumSeats = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tickets", x => x.Id); - table.ForeignKey( - name: "FK_Tickets_Customers_CustomerId", - column: x => x.CustomerId, - principalTable: "Customers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Tickets_Screenings_ScreeningId", - column: x => x.ScreeningId, - principalTable: "Screenings", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Screenings_MovieId", - table: "Screenings", - column: "MovieId"); - - migrationBuilder.CreateIndex( - name: "IX_Tickets_CustomerId", - table: "Tickets", - column: "CustomerId"); - - migrationBuilder.CreateIndex( - name: "IX_Tickets_ScreeningId", - table: "Tickets", - column: "ScreeningId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Tickets"); - - migrationBuilder.DropTable( - name: "Customers"); - - migrationBuilder.DropTable( - name: "Screenings"); - - migrationBuilder.DropTable( - name: "Movies"); - } - } -} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs index a1897d11..43407a49 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -22,6 +22,206 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => { b.Property("Id") @@ -152,6 +352,57 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Tickets"); }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => { b.HasOne("api_cinema_challenge.Models.Movie", "Movie") diff --git a/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs index 4120d5ab..cbee7397 100644 --- a/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs +++ b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs @@ -6,6 +6,6 @@ namespace api_cinema_challenge.Models { public class ApplicationUser : IdentityUser { - public RoleEnum Role { get; set; } + public Role Role { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 1ed4e43c..e55f0bad 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -3,26 +3,36 @@ using api_cinema_challenge.Models; using api_cinema_challenge.Repository; using api_cinema_challenge.Repository.Interfaces; +using api_cinema_challenge.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models; using Scalar.AspNetCore; using System.Diagnostics; using System.Diagnostics; +using System.Diagnostics; +using System.Text; using System.Text; using System.Text.Json.Serialization; +using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -builder.Services.AddOpenApi(); +// Add services +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); -/* builder.Services.AddSwaggerGen(option => { option.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); @@ -51,9 +61,9 @@ }); }); -*/ -// Dependency injection -//builder.Services.AddDbContext(); +builder.Services.AddProblemDetails(); +builder.Services.AddRouting(options => options.LowercaseUrls = true); + builder.Services.AddDbContext(options => { options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")) .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); @@ -72,7 +82,7 @@ opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); -/* + // Specify identity requirements // Must be added before .AddAuthentication otherwise a 404 is thrown on authorized endpoints builder.Services @@ -85,13 +95,10 @@ options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; }) - .AddRoles(); - //.AddEntityFrameworkStores(); // doesnt work + .AddRoles() + .AddEntityFrameworkStores(); -*/ -/* -// security // These will eventually be moved to a secrets file, but for alpha development appsettings is fine var validIssuer = builder.Configuration.GetValue("JwtTokenSettings:ValidIssuer"); var validAudience = builder.Configuration.GetValue("JwtTokenSettings:ValidAudience"); @@ -102,45 +109,44 @@ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; -}).AddJwtBearer(options => -{ - options.IncludeErrorDetails = true; - options.TokenValidationParameters = new TokenValidationParameters() +}) + .AddJwtBearer(options => { - ClockSkew = TimeSpan.Zero, - ValidateIssuer = true, - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidIssuer = validIssuer, - ValidAudience = validAudience, - IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(symmetricSecurityKey) - ), - }; -}); - -*/ + options.IncludeErrorDetails = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ClockSkew = TimeSpan.Zero, + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = validIssuer, + ValidAudience = validAudience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + }; + }); +// Build the app var app = builder.Build(); -// Configure the HTTP request pipeline. + +// Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { - app.MapOpenApi(); - app.UseSwaggerUI(options => - { - options.SwaggerEndpoint("/openapi/v1.json", "Demo API"); - }); - app.MapScalarApiReference(); + app.UseSwagger(); + app.UseSwaggerUI(); } app.UseHttpsRedirection(); +app.UseStatusCodePages(); -//app.UseAuthentication(); -//app.UseAuthorization(); +app.UseAuthentication(); +app.UseAuthorization(); -// endpoints configuration app.ConfigureCustomerEndpoint(); +app.ConfigureMovieEndpoint(); +app.MapControllers(); app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs new file mode 100644 index 00000000..ed9f8dc9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs @@ -0,0 +1,82 @@ +namespace api_cinema_challenge.Services; + +using api_cinema_challenge.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +public class TokenService +{ + private const int ExpirationMinutes = 60; + private readonly ILogger _logger; + public TokenService(ILogger logger) + { + _logger = logger; + } + + public string CreateToken(ApplicationUser user) + { + + var expiration = DateTime.UtcNow.AddMinutes(ExpirationMinutes); + var token = CreateJwtToken( + CreateClaims(user), + CreateSigningCredentials(), + expiration + ); + var tokenHandler = new JwtSecurityTokenHandler(); + + _logger.LogInformation("JWT Token created"); + + return tokenHandler.WriteToken(token); + } + + private JwtSecurityToken CreateJwtToken(List claims, SigningCredentials credentials, + DateTime expiration) => + new( + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidIssuer"], + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidAudience"], + claims, + expires: expiration, + signingCredentials: credentials + ); + + private List CreateClaims(ApplicationUser user) + { + var jwtSub = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["JwtRegisteredClaimNamesSub"]; + + try + { + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, jwtSub), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()) + }; + + return claims; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + private SigningCredentials CreateSigningCredentials() + { + var symmetricSecurityKey = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["SymmetricSecurityKey"]; + + return new SigningCredentials( + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + SecurityAlgorithms.HmacSha256 + ); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj index 751eb8b0..2d925e9f 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -8,15 +8,9 @@ 8499e9e9-9306-422f-a58d-332dfc8c5416 - - - - - - - + From feef2299d9dad8a58096b8d8cdfe9d8368514b32 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Mon, 25 Aug 2025 12:50:29 +0200 Subject: [PATCH 13/16] feat: seeder --- .../Data/CinemaContext.cs | 14 +- .../api-cinema-challenge/Data/Seeder.cs | 24 +- .../Migrations/CinemaContextModelSnapshot.cs | 265 ++++++++++++------ .../api-cinema-challenge/Program.cs | 9 +- 4 files changed, 206 insertions(+), 106 deletions(-) diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index fab6efcb..e051062c 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -9,7 +9,7 @@ namespace api_cinema_challenge.Data { // IdentityUserContext instead of Db in workshop - public class CinemaContext : IdentityDbContext + public class CinemaContext : IdentityUserContext { public CinemaContext(DbContextOptions options) : base(options) { @@ -17,8 +17,8 @@ public CinemaContext(DbContextOptions options) : base(options) protected override void OnModelCreating(ModelBuilder modelBuilder) { - base.OnModelCreating(modelBuilder); // ← This is crucial - // Optional: configure enum as string + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() .Property(u => u.Role) .HasConversion(); @@ -40,6 +40,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(s => s.MovieId); // seeder + var seeder = new Seeder(); + seeder.Seed(); + modelBuilder.Entity().HasData(seeder.Customers); + modelBuilder.Entity().HasData(seeder.Movies); + modelBuilder.Entity().HasData(seeder.Screenings); + modelBuilder.Entity().HasData(seeder.Tickets); + + } public DbSet Customers { get; set; } diff --git a/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs index 03870f27..47a11245 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs @@ -11,17 +11,17 @@ public class Seeder public void Seed() { - var customer1 = new Customer() { Id = 1, Name = "", Email = "", Phone = "" }; - var customer2 = new Customer() { Id = 2, Name = "", Email = "", Phone = "" }; - var customer3 = new Customer() { Id = 3, Name = "", Email = "", Phone = "" }; - var customer4 = new Customer() { Id = 4, Name = "", Email = "", Phone = "" }; - var customer5 = new Customer() { Id = 5, Name = "", Email = "", Phone = "" }; + var customer1 = new Customer() { Id = 1, Name = "Adam", Email = "a@a.com", Phone = "111" }; + var customer2 = new Customer() { Id = 2, Name = "Blazej", Email = "b@b.com", Phone = "222" }; + var customer3 = new Customer() { Id = 3, Name = "Kristian", Email = "c@c.com", Phone = "333" }; + var customer4 = new Customer() { Id = 4, Name = "Filip", Email = "d@c.com", Phone = "444" }; + var customer5 = new Customer() { Id = 5, Name = "Damian", Email = "e@e.com", Phone = "555" }; - var movie1 = new Movie() { Id = 1, Title = "", Rating = "", Description = "", RuntimeMins = 60 }; - var movie2 = new Movie() { Id = 2, Title = "", Rating = "", Description = "", RuntimeMins = 60 }; - var movie3 = new Movie() { Id = 3, Title = "", Rating = "", Description = "", RuntimeMins = 60 }; - var movie4 = new Movie() { Id = 4, Title = "", Rating = "", Description = "", RuntimeMins = 60 }; - var movie5 = new Movie() { Id = 5, Title = "", Rating = "", Description = "", RuntimeMins = 60 }; + var movie1 = new Movie() { Id = 1, Title = "Movie One", Rating = "PG13", Description = "fefef", RuntimeMins = 60 }; + var movie2 = new Movie() { Id = 2, Title = "Movie 2", Rating = "PG13", Description = "hrdr", RuntimeMins = 60 }; + var movie3 = new Movie() { Id = 3, Title = "333 movie", Rating = "PG13", Description = "esge", RuntimeMins = 60 }; + var movie4 = new Movie() { Id = 4, Title = "444 movie", Rating = "PG13", Description = "vesve", RuntimeMins = 60 }; + var movie5 = new Movie() { Id = 5, Title = "555 movie", Rating = "PG13", Description = "dwawd", RuntimeMins = 60 }; var screening1 = new Screening() { Id = 1, MovieId = 1, ScreenNumber = 1, Capacity = 50, StartsAt = DateTime.UtcNow }; var screening2 = new Screening() { Id = 2, MovieId = 1, ScreenNumber = 1, Capacity = 50, StartsAt = DateTime.UtcNow }; @@ -46,7 +46,7 @@ public void Seed() _movies.Add(movie4); _movies.Add(movie5); - _screenings.Add(screening2); + _screenings.Add(screening1); _screenings.Add(screening2); _tickets.Add(ticket1); @@ -58,7 +58,7 @@ public void Seed() } - public List Customer { get { return _customers; } } + public List Customers { get { return _customers; } } public List Movies { get { return _movies; } } public List Screenings { get { return _screenings; } } public List Tickets { get { return _tickets; } } diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs index 43407a49..b07488a8 100644 --- a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -22,57 +22,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { b.Property("Id") @@ -120,21 +69,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserLogins", (string)null); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("RoleId") - .HasColumnType("text"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { b.Property("UserId") @@ -251,6 +185,53 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.ToTable("Customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Email = "a@a.com", + Name = "Adam", + Phone = "111", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Email = "b@b.com", + Name = "Blazej", + Phone = "222", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Email = "c@c.com", + Name = "Kristian", + Phone = "333", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Email = "d@c.com", + Name = "Filip", + Phone = "444", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Email = "e@e.com", + Name = "Damian", + Phone = "555", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); }); modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => @@ -285,6 +266,58 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.ToTable("Movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "fefef", + Rating = "PG13", + RuntimeMins = 60, + Title = "Movie One", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "hrdr", + Rating = "PG13", + RuntimeMins = 60, + Title = "Movie 2", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "esge", + Rating = "PG13", + RuntimeMins = 60, + Title = "333 movie", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "vesve", + Rating = "PG13", + RuntimeMins = 60, + Title = "444 movie", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "dwawd", + Rating = "PG13", + RuntimeMins = 60, + Title = "555 movie", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); }); modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => @@ -318,6 +351,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("MovieId"); b.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 50, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 1, + ScreenNumber = 1, + StartsAt = new DateTime(2025, 8, 25, 10, 49, 52, 159, DateTimeKind.Utc).AddTicks(3594), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Capacity = 50, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 1, + ScreenNumber = 1, + StartsAt = new DateTime(2025, 8, 25, 10, 49, 52, 159, DateTimeKind.Utc).AddTicks(3695), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); }); modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => @@ -350,15 +405,62 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ScreeningId"); b.ToTable("Tickets"); - }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 1, + NumSeats = 1, + ScreeningId = 1, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 2, + NumSeats = 1, + ScreeningId = 1, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 3, + NumSeats = 1, + ScreeningId = 1, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 1, + NumSeats = 1, + ScreeningId = 2, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 2, + NumSeats = 1, + ScreeningId = 2, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 6, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 3, + NumSeats = 1, + ScreeningId = 2, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => @@ -379,21 +481,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55f0bad..88d5cd88 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -63,6 +63,7 @@ builder.Services.AddProblemDetails(); builder.Services.AddRouting(options => options.LowercaseUrls = true); +builder.Services.AddOpenApi(); builder.Services.AddDbContext(options => { options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")) @@ -135,8 +136,12 @@ // Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); + app.MapOpenApi(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/openapi/v1.json", "Demo API"); + }); + app.MapScalarApiReference(); } app.UseHttpsRedirection(); From 8e5512446c51ffaad2e3fdc30d5f748789daf773 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Mon, 25 Aug 2025 12:56:18 +0200 Subject: [PATCH 14/16] fix: DI tokenservice --- api-cinema-challenge/api-cinema-challenge/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 88d5cd88..24c18e1a 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -76,6 +76,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Support string to enum conversions builder.Services.AddControllers().AddJsonOptions(opt => From 4ae85654fab3d141d7599707a23616bafae2bf6b Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Mon, 25 Aug 2025 13:00:31 +0200 Subject: [PATCH 15/16] feat: endpoint authorization --- .../Endpoints/CustomerEndpoint.cs | 13 +++++++++++++ .../api-cinema-challenge/Endpoints/MovieEndpoint.cs | 13 +++++++++++++ .../api-cinema-challenge/Endpoints/UserEndpoint.cs | 6 ------ 3 files changed, 26 insertions(+), 6 deletions(-) delete mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/UserEndpoint.cs diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs index fe5ff2b0..d3e6c00e 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -4,6 +4,7 @@ using api_cinema_challenge.Models; using api_cinema_challenge.Repository.Interfaces; using api_cinema_challenge.Utils; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace api_cinema_challenge.Endpoints @@ -26,6 +27,7 @@ public static void ConfigureCustomerEndpoint(this WebApplication app) customerGroup.MapGet("/{customerId}/screenings/{screeningId}", GetTickets); } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] private static async Task GetAllCustomers(ICustomerRepository repository) { @@ -40,7 +42,9 @@ private static async Task GetAllCustomers(ICustomerRepository repositor return TypedResults.Ok(new { Status = "success", Data = dtos}); } + [Authorize] [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] private static async Task CreateCustomer(ICustomerRepository repository, HttpRequest request) { CustomerPostDto inDto = await Utility.ValidateFromRequest(request); @@ -63,7 +67,10 @@ private static async Task CreateCustomer(ICustomerRepository repository return TypedResults.Created(url, new {status = "success", data = outDto}); } + [Authorize] [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] private static async Task UpdateCustomer(ICustomerRepository repository, HttpRequest request, int id) { CustomerPutDto inDto = await Utility.ValidateFromRequest(request); @@ -88,7 +95,9 @@ private static async Task UpdateCustomer(ICustomerRepository repository return TypedResults.Created(); } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] private static async Task DeleteCustomer(ICustomerRepository repository, int customerId) { var customer = await repository.DeleteCustomer(customerId); @@ -101,7 +110,9 @@ private static async Task DeleteCustomer(ICustomerRepository repository return TypedResults.Ok( new { status = "success", data = dto}); } + [Authorize] [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] private static async Task BookTicket(ITicketRepository ticketRepository, HttpRequest request, int customerId, int screeningId) { TicketPostDto inDto = await Utility.ValidateFromRequest(request); @@ -120,7 +131,9 @@ private static async Task BookTicket(ITicketRepository ticketRepository return TypedResults.Created(); } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] private static async Task GetTickets(ITicketRepository ticketRepository, HttpRequest request, int customerId, int screeningId) { var tickets = await ticketRepository.GetByIdAsync(customerId, screeningId); diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs index b774abac..e6b86f09 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -6,6 +6,7 @@ using api_cinema_challenge.Models; using api_cinema_challenge.Repository.Interfaces; using api_cinema_challenge.Utils; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace api_cinema_challenge.Endpoints @@ -28,6 +29,7 @@ public static void ConfigureMovieEndpoint(this WebApplication app) moviesGroup.MapGet("/{movieId}/screenings", GetScreenings); } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] private static async Task GetAllMovies(IMovieRepository repository) { @@ -42,7 +44,9 @@ private static async Task GetAllMovies(IMovieRepository repository) return TypedResults.Ok(new { Status = "success", Data = dtos }); } + [Authorize] [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] private static async Task CreateMovie(IMovieRepository repository, HttpRequest request) { MoviePostDto inDto = await Utility.ValidateFromRequest(request); @@ -64,7 +68,10 @@ private static async Task CreateMovie(IMovieRepository repository, Http return TypedResults.Created(url, new { status = "success", data = outDto }); } + [Authorize] [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] private static async Task UpdateMovie(IMovieRepository repository, HttpRequest request, int movieId) { MoviePutDto inDto = await Utility.ValidateFromRequest(request); @@ -89,7 +96,9 @@ private static async Task UpdateMovie(IMovieRepository repository, Http return TypedResults.Created(); } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] private static async Task DeleteMovie(IMovieRepository repository, int movieId) { var movie = await repository.DeleteMovie(movieId); @@ -102,7 +111,9 @@ private static async Task DeleteMovie(IMovieRepository repository, int return TypedResults.Ok(new { status = "success", data = dto }); } + [Authorize] [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] private static async Task CreateScreening(IScreeningRepository repository, HttpRequest request, int movieId) { ScreeningPostDto inDto = await Utility.ValidateFromRequest(request); @@ -121,7 +132,9 @@ private static async Task CreateScreening(IScreeningRepository reposito return TypedResults.Created(); } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] private static async Task GetScreenings(IScreeningRepository repository, HttpRequest request, int movieId) { var screenings = await repository.GetByIdAsync(movieId); diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/UserEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/UserEndpoint.cs deleted file mode 100644 index 8e36ead7..00000000 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/UserEndpoint.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace api_cinema_challenge.Endpoints -{ - public class UserEndpoint - { - } -} From 662ae7c21bff26c69d6a68077799f91f4f4fbe82 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Mon, 25 Aug 2025 14:28:08 +0200 Subject: [PATCH 16/16] fix: swagger instead of open ai for auth swagger --- .../Controllers/UserController.cs | 2 +- .../api-cinema-challenge/Program.cs | 38 ++++++++++--------- .../api-cinema-challenge.csproj | 2 +- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs index 21cca1a0..837a07fa 100644 --- a/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs +++ b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs @@ -10,7 +10,7 @@ namespace api_cinema_challenge.Controllers { [ApiController] - [Route("/api/[controller]")] + [Route("api/[controller]")] public class UsersController : ControllerBase { private readonly UserManager _userManager; diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 24c18e1a..9e7a457d 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -5,27 +5,14 @@ using api_cinema_challenge.Repository.Interfaces; using api_cinema_challenge.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Models; -using Scalar.AspNetCore; -using System.Diagnostics; using System.Diagnostics; -using System.Diagnostics; -using System.Text; using System.Text; using System.Text.Json.Serialization; -using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); @@ -63,7 +50,6 @@ builder.Services.AddProblemDetails(); builder.Services.AddRouting(options => options.LowercaseUrls = true); -builder.Services.AddOpenApi(); builder.Services.AddDbContext(options => { options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")) @@ -130,20 +116,36 @@ }; }); +// policy-based authorization for Admin and User roles +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("Admin", policy => + policy.RequireRole("Admin")); + options.AddPolicy("User", policy => + policy.RequireRole("User")); +}); + + // Build the app var app = builder.Build(); - // Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { - app.MapOpenApi(); + app.UseSwagger(); app.UseSwaggerUI(options => { - options.SwaggerEndpoint("/openapi/v1.json", "Demo API"); + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Test API v1"); + options.RoutePrefix = "swagger"; }); - app.MapScalarApiReference(); + } +app.UseSwagger(); +app.UseSwaggerUI(options => +{ + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Test API v1"); + options.RoutePrefix = "swagger"; +}); app.UseHttpsRedirection(); app.UseStatusCodePages(); 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 2d925e9f..dc9602ea 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -20,7 +20,7 @@ - +