diff --git a/ERD_Cinema.drawio.png b/ERD_Cinema.drawio.png new file mode 100644 index 00000000..8e7a497b Binary files /dev/null and b/ERD_Cinema.drawio.png differ diff --git a/api-cinema-challenge/api-cinema-challenge/Controllers/UsersController.cs b/api-cinema-challenge/api-cinema-challenge/Controllers/UsersController.cs new file mode 100644 index 00000000..394239be --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Controllers/UsersController.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using workshop.webapi.Services; +using workshop.webapi.DataTransfer.Response; +using workshop.webapi.DataTransfer.Requests; +using api_cinema_challenge.Enums; + +namespace workshop.webapi.Controllers +{ + + [ApiController] + [Route("/api/[controller]")] + public class UsersController : ControllerBase + { + private readonly UserManager _userManager; + private readonly CinemaContext _context; + private readonly TokenService _tokenService; + + public UsersController(UserManager userManager, CinemaContext context, + TokenService tokenService, ILogger logger) + { + _userManager = userManager; + _context = context; + _tokenService = tokenService; + } + + + [HttpPost] + [Route("register")] + public async Task Register(RegistrationRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var result = await _userManager.CreateAsync( + new ApplicationUser { UserName = request.Username, Email = request.Email, Role = request.Role }, + request.Password! + ); + + if (result.Succeeded) + { + request.Password = ""; + return CreatedAtAction(nameof(Register), new { email = request.Email, role = Role.User }, request); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(error.Code, error.Description); + } + + return BadRequest(ModelState); + } + + + [HttpPost] + [Route("login")] + public async Task> Authenticate([FromBody] AuthRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var managedUser = await _userManager.FindByEmailAsync(request.Email!); + + if (managedUser == null) + { + return BadRequest("Bad credentials"); + } + + var isPasswordValid = await _userManager.CheckPasswordAsync(managedUser, request.Password!); + + if (!isPasswordValid) + { + return BadRequest("Bad credentials"); + } + + var userInDb = _context.Users.FirstOrDefault(u => u.Email == request.Email); + + if (userInDb is null) + { + return Unauthorized(); + } + + var accessToken = _tokenService.CreateToken(userInDb); + await _context.SaveChangesAsync(); + + return Ok(new AuthResponse + { + Username = userInDb.UserName, + Email = userInDb.Email, + Token = accessToken, + }); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPost.cs new file mode 100644 index 00000000..7b36bb48 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPost.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs +{ + public class CustomerPost + { + 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/CustomerPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs new file mode 100644 index 00000000..f02ae3ee --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs +{ + public class CustomerPut + { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs new file mode 100644 index 00000000..a765657e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs +{ + public class MoviePost + { + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs new file mode 100644 index 00000000..c8005d5e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs +{ + public class MoviePut + { + 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/ScreeningGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs new file mode 100644 index 00000000..9e12cfde --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs @@ -0,0 +1,11 @@ +namespace api_cinema_challenge.DTOs +{ + public class ScreeningGet + { + public int Id { get; set; } + public int MovieId { get; set; } + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs new file mode 100644 index 00000000..f58f4eac --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs +{ + public class ScreeningPost + { + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index ad4fe854..6c233586 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,26 +1,100 @@ -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json.Linq; +using api_cinema_challenge.Models; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace api_cinema_challenge.Data { - public class CinemaContext : DbContext + public class CinemaContext : IdentityUserContext { 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) { + modelBuilder.Entity().HasData( + new Customer + { + Id = 1, + Name = "Isabell Tran", + Email = "isabell@experis.com", + Phone = "12345678", + CreatedAt = new DateTime(2025, 08, 22, 10, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 22, 10, 00, 00, DateTimeKind.Utc) + } + ); + modelBuilder.Entity().HasData( + new Customer + { + Id = 2, + Name = "Marie Hansen", + Email = "Marie@experis.com", + Phone = "98989898", + CreatedAt = new DateTime(2025, 08, 23, 10, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 23, 10, 00, 00, DateTimeKind.Utc) + } + ); + + modelBuilder.Entity().HasData( + new Movie + { + Id = 1, + Title = "Inception", + Rating = "PG-13", + Description = "A mind-bending thriller", + RuntimeMins = 148, + CreatedAt = new DateTime(2025, 08, 22, 15, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 22, 15, 00, 00, DateTimeKind.Utc) + } + ); + + modelBuilder.Entity().HasData( + new Movie + { + Id = 2, + Title = "F1", + Rating = "PG-13", + Description = "Action", + RuntimeMins = 155, + CreatedAt = new DateTime(2025, 08, 22, 15, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 22, 15, 00, 00, DateTimeKind.Utc) + } + ); + + modelBuilder.Entity().HasData( + new Screening + { + Id = 1, + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 08, 22, 14, 00, 00, DateTimeKind.Utc), + Capacity = 100, + CreatedAt = new DateTime(2025, 08, 22, 14, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 22, 14, 00, 00, DateTimeKind.Utc) + } + ); + + modelBuilder.Entity().HasData( + new Ticket + { + Id = 1, + CustomerId = 1, + ScreeningId = 1, + NumSeats = 2, + CreatedAt = new DateTime(2025, 08, 22, 13, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 22, 13, 00, 00, DateTimeKind.Utc) + } + ); + base.OnModelCreating(modelBuilder); + } + + 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/DataTransfer/Requests/AuthRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs new file mode 100644 index 00000000..c5f0c2e9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs @@ -0,0 +1,12 @@ +namespace workshop.webapi.DataTransfer.Requests; + +public class AuthRequest +{ + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() + { + return true; + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs new file mode 100644 index 00000000..7f706f29 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs @@ -0,0 +1,19 @@ + + +using api_cinema_challenge.Enums; +using System.ComponentModel.DataAnnotations; + + +public class RegistrationRequest +{ + [Required] + public string? Email { get; set; } + + [Required] + public string? Username { get { return this.Email; } set { } } + + [Required] + public string? Password { get; set; } + + public Role Role { get; set; } = Role.User; +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs new file mode 100644 index 00000000..f108e151 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs @@ -0,0 +1,9 @@ +namespace workshop.webapi.DataTransfer.Response; + + +public class AuthResponse +{ + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs new file mode 100644 index 00000000..dbe69926 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs @@ -0,0 +1,79 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class CustomerEndpoints + { + public static void ConfigureCustomerEndpoint(this WebApplication app) + { + var customerGroup = app.MapGroup("customer").RequireAuthorization(); + + customerGroup.MapPost("/", AddCustomer); + customerGroup.MapGet("/", GetCustomers); + customerGroup.MapPut("/{id}", UpdateCustomer); + customerGroup.MapDelete("/{id}", DeleteCustomer); + + } + + // Adding Authorize + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task AddCustomer(IRepository repository, CustomerPost customer) + { + var results = await repository.GetCustomers(); + if (results.Any(x => x.Name.Equals(customer.Name, StringComparison.OrdinalIgnoreCase))) + { + return Results.BadRequest("Customer with provided name already exists"); + } + Customer entity = new Customer(); + entity.Name = customer.Name; + entity.Email = customer.Email; + entity.Phone = customer.Phone; + entity.CreatedAt = DateTime.UtcNow; + entity.UpdatedAt = DateTime.UtcNow; + await repository.AddCustomer(entity); + + return TypedResults.Created($"https://localhost:7195/customer/{entity.Id}", new { Name = customer.Name, Email = customer.Email, Phone = customer.Phone, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }); + + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task GetCustomers(IRepository repository) + { + var results = await repository.GetCustomers(); + return TypedResults.Ok(results); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task UpdateCustomer(IRepository repository, int id, CustomerPut customer) + { + var entity = await repository.GetCustomerId(id); + + if (customer.Name != null) entity.Name = customer.Name; + if (customer.Email != null) entity.Email = customer.Email; + if (customer.Phone != null) entity.Phone = customer.Phone; + entity.UpdatedAt = DateTime.UtcNow; + entity.CreatedAt = DateTime.UtcNow; + + await repository.UpdateCustomer(id, entity); + + return TypedResults.Created($"https://localhost:7195/customer/{entity.Id}", new { Name = customer.Name, Email = customer.Email, Phone = customer.Phone, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task DeleteCustomer(IRepository repository, int id) + { + var entity = await repository.DeleteCustomer(id); + return TypedResults.Ok(entity); + } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs new file mode 100644 index 00000000..4b7f2d9f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs @@ -0,0 +1,104 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class MovieEndpoints + { + public static void ConfigureMovieEndpoint(this WebApplication app) + { + var movieGroup = app.MapGroup("movie").RequireAuthorization(); + + movieGroup.MapPost("/", AddMovie); + movieGroup.MapGet("/", GetMovies); + movieGroup.MapPut("/{id}", UpdateMovie); + movieGroup.MapDelete("/{id}", DeleteMovie); + movieGroup.MapPost("/{id}/screenings", AddScreening); + movieGroup.MapGet("/{id}/screenings", GetScreenings); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task AddMovie(IRepository repository, MoviePost movie) + { + Movie entity = new Movie(); + entity.Title = movie.Title; + entity.Rating = movie.Rating; + entity.Description = movie.Description; + entity.RuntimeMins = movie.RuntimeMins; + entity.CreatedAt = DateTime.UtcNow; + entity.UpdatedAt = DateTime.UtcNow; + var results = await repository.AddMovie(entity); + return TypedResults.Created($"https://localhost:7195/movie/{entity.Id}", new { Title = movie.Title, Rating = movie.Rating, Description = movie.Description, RuntimeMins = movie.RuntimeMins, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }); + + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task GetMovies(IRepository repository) + { + var results = await repository.GetMovies(); + return TypedResults.Ok(results); + } + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + + public static async Task UpdateMovie(IRepository repository, int id, MoviePut movie) + { + var entity = await repository.GetMovieId(id); + + if (movie.Title != null) entity.Title = movie.Title; + if (movie.Rating != null) entity.Rating = movie.Rating; + if (movie.Description != null) entity.Description = movie.Description; + if (movie.RuntimeMins != null) entity.RuntimeMins = movie.RuntimeMins; + entity.UpdatedAt = DateTime.UtcNow; + entity.CreatedAt = DateTime.UtcNow; + + await repository.UpdateMovie(id, entity); + return TypedResults.Created($"https://localhost:7195/movie/{entity.Id}", new { Title = movie.Title, Rating = movie.Rating, Description = movie.Description, RuntimeMins = movie.RuntimeMins, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }); + + } + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task DeleteMovie(IRepository repository, int id) + { + var results = await repository.DeleteMovie(id); + return TypedResults.Ok(results); + } + + // Screenings + + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task AddScreening(IRepository repository, ScreeningPost screening, int movieId) + { + Screening entity = new Screening(); + entity.ScreenNumber = screening.ScreenNumber; + entity.Capacity = screening.Capacity; + entity.StartsAt = screening.StartsAt; + entity.CreatedAt = DateTime.UtcNow; + entity.UpdatedAt = DateTime.UtcNow; + var results = await repository.AddScreening(entity); + return TypedResults.Created($"https://localhost:7195/movie/{movieId}/screenings", new { Id = entity.Id, ScreenNumber = screening.ScreenNumber, Capacity = screening.Capacity, StartsAt = screening.StartsAt, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }); + + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task GetScreenings(IRepository repository, int movieId) + { + List screens = new List(); + var results = await repository.GetScreenings(); + foreach (Screening screen in results) + { + if (movieId == screen.MovieId) + { + screens.Add(new ScreeningGet() {Id = screen.Id, ScreenNumber = screen.ScreenNumber, Capacity = screen.Capacity, StartsAt = screen.StartsAt }); + } + } + return TypedResults.Ok(screens); + } + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs new file mode 100644 index 00000000..551a6178 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.Enums +{ + public enum Role + { + Admin, + User + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs new file mode 100644 index 00000000..e394077e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; +using System.Security.Claims; + +namespace workshop.webapi +{ + public static class ClaimsPrincipalHelper + { + public static string? UserId(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.NameIdentifier); + return claim?.Value; + } + public static string? Email(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.Email); + return claim?.Value; + } + + // public static string? UserId(this IIdentity identity) + // { + // if (identity != null && identity.IsAuthenticated) + // { + // // return Guid.Parse(((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value); + // return ((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value; + // } + // return null; + // } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.Designer.cs new file mode 100644 index 00000000..bb798009 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.Designer.cs @@ -0,0 +1,200 @@ +// +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("20250822123432_seond")] + partial class seond + { + /// + 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"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "isabell@experis.com", + Name = "Isabell Tran", + Phone = "12345678", + UpdatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "A mind-bending thriller", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), + CustomerId = 1, + NumSeats = 2, + ScreeningId = 1, + UpdatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.cs new file mode 100644 index 00000000..a7793255 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.cs @@ -0,0 +1,122 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class seond : 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), + StartsAt = table.Column(type: "timestamp with time zone", nullable: false), + Capacity = 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_Screenings", x => x.Id); + }); + + 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); + }); + + migrationBuilder.InsertData( + table: "Customers", + columns: new[] { "Id", "CreatedAt", "Email", "Name", "Phone", "UpdatedAt" }, + values: new object[] { 1, new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), "isabell@experis.com", "Isabell Tran", "12345678", new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) }); + + migrationBuilder.InsertData( + table: "Movies", + columns: new[] { "Id", "CreatedAt", "Description", "Rating", "RuntimeMins", "Title", "UpdatedAt" }, + values: new object[] { 1, new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), "A mind-bending thriller", "PG-13", 148, "Inception", new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) }); + + migrationBuilder.InsertData( + table: "Screenings", + columns: new[] { "Id", "Capacity", "CreatedAt", "MovieId", "ScreenNumber", "StartsAt", "UpdatedAt" }, + values: new object[] { 1, 100, new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), 1, 5, new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) }); + + migrationBuilder.InsertData( + table: "Tickets", + columns: new[] { "Id", "CreatedAt", "CustomerId", "NumSeats", "ScreeningId", "UpdatedAt" }, + values: new object[] { 1, new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), 1, 2, 1, new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Movies"); + + migrationBuilder.DropTable( + name: "Screenings"); + + migrationBuilder.DropTable( + name: "Tickets"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.Designer.cs new file mode 100644 index 00000000..27e669f4 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.Designer.cs @@ -0,0 +1,219 @@ +// +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("20250825064900_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"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "isabell@experis.com", + Name = "Isabell Tran", + Phone = "12345678", + UpdatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "Marie@experis.com", + Name = "Marie Hansen", + Phone = "98989898", + UpdatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "A mind-bending thriller", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "Action", + Rating = "PG-13", + RuntimeMins = 155, + Title = "F1", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), + CustomerId = 1, + NumSeats = 2, + ScreeningId = 1, + UpdatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.cs new file mode 100644 index 00000000..5b99799b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class first : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Customers", + columns: new[] { "Id", "CreatedAt", "Email", "Name", "Phone", "UpdatedAt" }, + values: new object[] { 2, new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc), "Marie@experis.com", "Marie Hansen", "98989898", new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc) }); + + migrationBuilder.InsertData( + table: "Movies", + columns: new[] { "Id", "CreatedAt", "Description", "Rating", "RuntimeMins", "Title", "UpdatedAt" }, + values: new object[] { 2, new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), "Action", "PG-13", 155, "F1", new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Customers", + keyColumn: "Id", + keyValue: 2); + + migrationBuilder.DeleteData( + table: "Movies", + keyColumn: "Id", + keyValue: 2); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.Designer.cs new file mode 100644 index 00000000..e02c85aa --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.Designer.cs @@ -0,0 +1,219 @@ +// +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("20250825081039_third")] + partial class third + { + /// + 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"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "isabell@experis.com", + Name = "Isabell Tran", + Phone = "12345678", + UpdatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "Marie@experis.com", + Name = "Marie Hansen", + Phone = "98989898", + UpdatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "A mind-bending thriller", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "Action", + Rating = "PG-13", + RuntimeMins = 155, + Title = "F1", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), + CustomerId = 1, + NumSeats = 2, + ScreeningId = 1, + UpdatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.cs new file mode 100644 index 00000000..db92d939 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class third : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.Designer.cs new file mode 100644 index 00000000..60a84007 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.Designer.cs @@ -0,0 +1,379 @@ +// +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("20250825114610_foruth")] + partial class foruth + { + /// + 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("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.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") + .HasColumnType("integer"); + + 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") + .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"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "isabell@experis.com", + Name = "Isabell Tran", + Phone = "12345678", + UpdatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "Marie@experis.com", + Name = "Marie Hansen", + Phone = "98989898", + UpdatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "A mind-bending thriller", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "Action", + Rating = "PG-13", + RuntimeMins = 155, + Title = "F1", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), + CustomerId = 1, + NumSeats = 2, + ScreeningId = 1, + UpdatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.IdentityUserToken", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.cs new file mode 100644 index 00000000..0756fc8d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.cs @@ -0,0 +1,140 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class foruth : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Role = table.Column(type: "integer", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} 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..521da52f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,376 @@ +// +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("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.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") + .HasColumnType("integer"); + + 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") + .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"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "isabell@experis.com", + Name = "Isabell Tran", + Phone = "12345678", + UpdatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "Marie@experis.com", + Name = "Marie Hansen", + Phone = "98989898", + UpdatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "A mind-bending thriller", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "Action", + Rating = "PG-13", + RuntimeMins = 155, + Title = "F1", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), + CustomerId = 1, + NumSeats = 2, + ScreeningId = 1, + UpdatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + 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.IdentityUserToken", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#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..cbee7397 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs @@ -0,0 +1,11 @@ +using api_cinema_challenge.Enums; +using Microsoft.AspNetCore.Identity; +using System.Data; + +namespace api_cinema_challenge.Models +{ + public class ApplicationUser : IdentityUser + { + public Role Role { get; set; } + } +} 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..cb2a838e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.Models +{ + public class Customer + { + 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/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs new file mode 100644 index 00000000..34970903 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,13 @@ +namespace api_cinema_challenge.Models +{ + 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 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 new file mode 100644 index 00000000..17fd44d6 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,13 @@ +namespace api_cinema_challenge.Models +{ + public class Screening + { + public int Id { get; set; } + public int MovieId { get; set; } + public int ScreenNumber { get; set; } + public DateTime StartsAt { get; set; } + public int Capacity { get; set; } + 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 new file mode 100644 index 00000000..c4485faf --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.Models +{ + public class Ticket + { + public int Id { get; set; } + public int CustomerId { get; set; } + public int ScreeningId { 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/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..23e906e5 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,15 +1,121 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using System.Diagnostics; +using System.Text; +using System.Text.Json.Serialization; +using workshop.webapi.Services; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. +// Add services +builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -builder.Services.AddDbContext(); +builder.Services.AddSwaggerGen(option => +{ + option.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); + option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter a valid token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + option.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); +}); + +builder.Services.AddProblemDetails(); +builder.Services.AddApiVersioning(); +builder.Services.AddRouting(options => options.LowercaseUrls = true); +builder.Services.AddDbContext(opt => +{ + opt.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); + opt.LogTo(message => Debug.WriteLine(message)); + +}); +builder.Services.AddScoped(); +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(); + + +// 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) + ), + }; + }); + +// Build the app var app = builder.Build(); -// Configure the HTTP request pipeline. + +// Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.UseSwagger(); @@ -17,4 +123,13 @@ } app.UseHttpsRedirection(); -app.Run(); +app.UseStatusCodePages(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.ConfigureCustomerEndpoint(); +app.ConfigureMovieEndpoint(); + +app.MapControllers(); +app.Run(); \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs new file mode 100644 index 00000000..35a60a5e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs @@ -0,0 +1,27 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository +{ + public interface IRepository + { + // Customer + Task AddCustomer(Customer customer); + Task> GetCustomers(); + Task GetCustomerId(int id); + Task UpdateCustomer(int id, Customer customer); + Task DeleteCustomer(int id); + + // Movie + Task AddMovie(Movie movie); + Task> GetMovies(); + Task GetMovieId(int id); + Task UpdateMovie(int id, Movie movie); + Task DeleteMovie(int id); + + // Screening + Task AddScreening(Screening screening); + Task> GetScreenings(); + } +} + + diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs new file mode 100644 index 00000000..0f355a62 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs @@ -0,0 +1,93 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository +{ + public class Repository : IRepository + { + private CinemaContext _context; + public Repository(CinemaContext context) + { + _context = context; + } + + // Customer + public async Task AddCustomer(Customer customer) + { + await _context.Customers.AddAsync(customer); + await _context.SaveChangesAsync(); + return customer; + } + public async Task GetCustomerId(int id) + { + return await _context.Customers.FindAsync(id); + } + public async Task> GetCustomers() + { + return await _context.Customers.ToListAsync(); + } + public async Task UpdateCustomer(int id, Customer customer) + { + var target = await _context.Customers.FindAsync(id); + target = customer; + await _context.SaveChangesAsync(); + return target; + } + public async Task DeleteCustomer(int id) + { + var target = await _context.Customers.FindAsync(id); + if (target == null) return null; + _context.Customers.Remove(target); + await _context.SaveChangesAsync(); + return target; + } + + // Movie + public async Task AddMovie(Movie movie) + { + await _context.Movies.AddAsync(movie); + await _context.SaveChangesAsync(); + return movie; + } + + public async Task> GetMovies() + { + return await _context.Movies.ToListAsync(); + } + + public async Task GetMovieId(int id) + { + return await _context.Movies.FindAsync(id); + } + + public async Task UpdateMovie(int id, Movie movie) + { + var target = await _context.Movies.FindAsync(id); + target = movie; + await _context.SaveChangesAsync(); + return target; + } + + public async Task DeleteMovie(int id) + { + var target = await _context.Movies.FindAsync(id); + if (target == null) return null; + _context.Movies.Remove(target); + await _context.SaveChangesAsync(); + return target; + } + + public async Task AddScreening(Screening screening) + { + await _context.Screenings.AddAsync(screening); + await _context.SaveChangesAsync(); + return screening; + } + + public async Task> GetScreenings() + { + return await _context.Screenings.ToListAsync(); + } + } +} 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..aa4411cc --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs @@ -0,0 +1,80 @@ +namespace workshop.webapi.Services; + +using api_cinema_challenge.Models; +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 + ); + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj index 11e5c66b..d7b0a739 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -8,15 +8,15 @@ - - - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -27,8 +27,4 @@ - - - - diff --git a/api-cinema-challenge/api-cinema-challenge/appsettings.example.json b/api-cinema-challenge/api-cinema-challenge/appsettings.example.json deleted file mode 100644 index b9175fe6..00000000 --- a/api-cinema-challenge/api-cinema-challenge/appsettings.example.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "ConnectionStrings": { - "DefaultConnectionString": "Host=HOST; Database=DATABASE; Username=USERNAME; Password=PASSWORD;" - } -} \ No newline at end of file