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..77b26da7 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs @@ -0,0 +1,99 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.DataTransfer.Requests; +using api_cinema_challenge.DataTransfer.Response; +using api_cinema_challenge.Enums; +using api_cinema_challenge.Models; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.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/Customers/CustomerDTO.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Customers/CustomerDTO.cs new file mode 100644 index 00000000..d8a3803d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Customers/CustomerDTO.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.DTOs.Customers +{ + 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/Customers/CustomerPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Customers/CustomerPost.cs new file mode 100644 index 00000000..6ac86b59 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Customers/CustomerPost.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs.Customers +{ + 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/Customers/CustomerPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Customers/CustomerPut.cs new file mode 100644 index 00000000..27211ec1 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Customers/CustomerPut.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs.Customers +{ + 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/Movies/MovieDTO.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MovieDTO.cs new file mode 100644 index 00000000..66b994b0 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MovieDTO.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.DTOs.Movies +{ + 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 DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MoviePost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MoviePost.cs new file mode 100644 index 00000000..4921e727 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MoviePost.cs @@ -0,0 +1,13 @@ +using api_cinema_challenge.DTOs.Screenings; + +namespace api_cinema_challenge.DTOs.Movies +{ + public class MoviePost + { + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + public List? screenings { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MoviePut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MoviePut.cs new file mode 100644 index 00000000..349f2ec6 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MoviePut.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs.Movies +{ + 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/Movies/MovieWithScreenings.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MovieWithScreenings.cs new file mode 100644 index 00000000..332dbaf9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MovieWithScreenings.cs @@ -0,0 +1,10 @@ +using api_cinema_challenge.DTOs.Screenings; + +namespace api_cinema_challenge.DTOs.Movies +{ + public class MovieWithScreenings + { + public int Id { get; set; } + public List Screenings { get; set; } = new List(); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Screenings/ScreeningPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Screenings/ScreeningPost.cs new file mode 100644 index 00000000..91c38b26 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Screenings/ScreeningPost.cs @@ -0,0 +1,13 @@ +using api_cinema_challenge.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.DTOs.Screenings +{ + 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/DTOs/Screenings/ScreeningsForMovie.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Screenings/ScreeningsForMovie.cs new file mode 100644 index 00000000..20b5d978 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Screenings/ScreeningsForMovie.cs @@ -0,0 +1,15 @@ +using api_cinema_challenge.Models; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.DTOs.Screenings +{ + public class ScreeningsForMovie + { + 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; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Tickets/TicketDTO.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Tickets/TicketDTO.cs new file mode 100644 index 00000000..65ed7a81 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Tickets/TicketDTO.cs @@ -0,0 +1,14 @@ +using api_cinema_challenge.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.DTOs.Tickets +{ + public class TicketDTO + { + public int Id { get; set; } + public int numSeats { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Tickets/TicketPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Tickets/TicketPost.cs new file mode 100644 index 00000000..d39a7893 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Tickets/TicketPost.cs @@ -0,0 +1,7 @@ +namespace api_cinema_challenge.DTOs.Tickets +{ + public class TicketPost + { + public int numSeats { 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..7109f2d5 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,16 +1,18 @@ -using Microsoft.EntityFrameworkCore; +using api_cinema_challenge.Models; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; 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(); + //this.Database.EnsureCreated(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) @@ -20,7 +22,39 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasData( + new Customer { Id = 1, Name = "John Doe", Email = "john.doe@email.com", Phone = "+4792763498", CreatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc), UpdatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc) }, + new Customer { Id = 2, Name = "Jane Doe", Email = "jane.doe@email.com", Phone = "+4743761209", CreatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc), UpdatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc) } + ); + + modelBuilder.Entity().HasData( + new Movie { Id = 1, Title = "Inception", Rating = "PG-13", Description = "The film stars Leonardo DiCaprio as a professional " + + "thief who steals information by infiltrating the subconscious of his targets. He is offered a chance to have his criminal " + + "history erased as payment for the implantation of another person's idea into a target's subconscious.", RuntimeMins = 148, + CreatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc)}, + new Movie { Id = 2, Title = "The Godfather", Rating = "R", Description = "\"The Godfather\" is based on Mario Puzo's novel of the " + + "same name. The film chronicles the life of the Corleone family, a powerful Italian-American mafia clan in New York City, focusing " + + "on the patriarch, Don Vito Corleone, and his youngest son, Michael Corleone.", RuntimeMins = 175, + CreatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc)} + ); + + modelBuilder.Entity().HasData( + new Screening { Id = 1, MovieId = 1, ScreenNumber = 5, Capacity = 100, StartsAt = new DateTime(2025, 10, 01, 11, 3, 0, DateTimeKind.Utc), + CreatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc)}, + new Screening { Id = 2, MovieId = 2, ScreenNumber = 3, Capacity = 150, StartsAt = new DateTime(2025, 10, 01, 12, 3, 0, DateTimeKind.Utc), + CreatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 15, 10, 0, 0, DateTimeKind.Utc)} + ); } + + 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..606db053 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs @@ -0,0 +1,13 @@ +namespace api_cinema_challenge.DataTransfer.Requests +{ + public class AuthRequest + { + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() + { + return true; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs new file mode 100644 index 00000000..693e1996 --- /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; + +namespace api_cinema_challenge.DataTransfer.Requests +{ + public class RegistrationRequest + { + [Required] + public string? Email { get; set; } + + [Required] + public string? Username { get { return this.Email; } set { } } + + [Required] + public string? Password { get; set; } + + public Role Role { get; set; } = Role.User; + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs new file mode 100644 index 00000000..b334a5a6 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DataTransfer.Response +{ + public class AuthResponse + { + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } + } +} 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..a06e1e9c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs @@ -0,0 +1,188 @@ +using api_cinema_challenge.DTOs.Customers; +using api_cinema_challenge.DTOs.Tickets; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace api_cinema_challenge.Endpoints +{ + public static class CustomerEndpoints + { + public static void ConfigureCustomerEndpoints(this WebApplication app) + { + var customersGroup = app.MapGroup("customers"); + + customersGroup.MapGet("/", GetCustomers); + customersGroup.MapPost("/", AddCustomer); + customersGroup.MapPut("/{id}", UpdateCustomer); + customersGroup.MapDelete("/{id}", DeleteCustomer); + + customersGroup.MapGet("/{customer_id}/screening/{screening_id}", GetTickets); + customersGroup.MapPost("/{customer_id}/screening/{screening_id}", AddTicket); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task GetCustomers(IRepository repository, ClaimsPrincipal user) + { + var customers = await repository.Get(); + + var response = new + { + Status = "success", + Data = customers.Select(customer => new CustomerDTO + { + Id = customer.Id, + Name = customer.Name, + Email = customer.Email, + Phone = customer.Phone, + CreatedAt = customer.CreatedAt, + UpdatedAt = customer.UpdatedAt + }).ToList() + }; + + return TypedResults.Ok(response); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task AddCustomer(IRepository repository, CustomerPost model, ClaimsPrincipal user) + { + Customer entity = new Customer(); + entity.Name = model.Name; + entity.Email = model.Email; + entity.Phone = model.Phone; + + await repository.Insert(entity); + + var response = new + { + Status = "success", + Data = new CustomerDTO + { + Id = entity.Id, + Name = entity.Name, + Email = entity.Email, + Phone = entity.Phone, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt + } + }; + + return TypedResults.Created($"https://localhost:7239/customers/{entity.Id}", response); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task UpdateCustomer(IRepository repository, int id, CustomerPut model, ClaimsPrincipal user) + { + var entity = await repository.GetById(id); + + if (model.Name != null) entity.Name = model.Name; + if (model.Email != null) entity.Email = model.Email; + if (model.Phone != null) entity.Phone = model.Phone; + entity.UpdatedAt = DateTime.UtcNow; + + await repository.Update(entity); + + var updatedEntity = await repository.GetById(id); + + var response = new + { + Status = "success", + Data = new CustomerDTO + { + Id = entity.Id, + Name = entity.Name, + Email = entity.Email, + Phone = entity.Phone, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt + } + }; + + return TypedResults.Created($"https://localhost:7239/customers/{entity.Id}", response); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task DeleteCustomer(IRepository repository, int id, ClaimsPrincipal user) + { + var entity = await repository.GetById(id); + + await repository.Delete(id); + + var response = new + { + Status = "success", + Data = new CustomerDTO + { + Id = entity.Id, + Name = entity.Name, + Email = entity.Email, + Phone = entity.Phone, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt + } + }; + + return TypedResults.Ok(response); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task GetTickets(IRepository repository, int customer_id, int screening_id, ClaimsPrincipal user) + { + var tickets = await repository.Get(); + tickets = tickets.Where(t => t.CustomerId == customer_id && t.ScreeningId == screening_id); + + var response = new + { + Status = "success", + Data = tickets.Select(ticket => new TicketDTO + { + Id = ticket.Id, + numSeats = ticket.numSeats, + CreatedAt = ticket.CreatedAt, + UpdatedAt = ticket.UpdatedAt + }).ToList() + }; + + return TypedResults.Ok(response); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task AddTicket(IRepository repository, TicketPost model, int customer_id, int screening_id, ClaimsPrincipal user) + { + Ticket entity = new Ticket(); + entity.CustomerId = customer_id; + entity.ScreeningId = screening_id; + entity.numSeats = model.numSeats; + + await repository.Insert(entity); + + var response = new + { + Status = "success", + Data = new TicketDTO + { + Id = entity.Id, + numSeats = entity.numSeats, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt + } + }; + + return TypedResults.Created($"https://localhost:7239/customers/{entity.CustomerId}/screenings/{entity.ScreeningId}", response); + } + } +} 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..a6b11855 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs @@ -0,0 +1,225 @@ +using api_cinema_challenge.DTOs.Customers; +using api_cinema_challenge.DTOs.Movies; +using api_cinema_challenge.DTOs.Screenings; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace api_cinema_challenge.Endpoints +{ + public static class MovieEndpoints + { + public static void ConfigureMovieEndpoints(this WebApplication app) + { + var moviesGroup = app.MapGroup("movies"); + + moviesGroup.MapGet("/", GetMovies); + moviesGroup.MapPost("/", AddMovie); + moviesGroup.MapPut("/{id}", UpdateMovie); + moviesGroup.MapDelete("/{id}", DeleteMovie); + + moviesGroup.MapGet("/{id}/screenings", GetScreeningsForMovie); + moviesGroup.MapPost("/{id}/screenings", AddScreeningToMovie); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task GetMovies(IRepository repository, ClaimsPrincipal user) + { + var movies = await repository.Get(); + + var response = new + { + status = "success", + data = movies.Select(movie => new MovieDTO + { + Id = movie.Id, + Title = movie.Title, + Rating = movie.Rating, + Description = movie.Description, + RuntimeMins = movie.RuntimeMins, + CreatedAt = movie.CreatedAt, + UpdatedAt = movie.UpdatedAt + }).ToList() + }; + + return TypedResults.Ok(response); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task AddMovie(IRepository movieRepository, IRepository screeningRepository, MoviePost model, ClaimsPrincipal user) + { + Movie entity = new Movie(); + entity.Title = model.Title; + entity.Rating = model.Rating; + entity.Description = model.Description; + entity.RuntimeMins = model.RuntimeMins; + + await movieRepository.Insert(entity); + + if (model.screenings != null && model.screenings.Count > 0) + { + foreach (var screening in model.screenings) + { + Screening newScreening = new Screening + { + MovieId = entity.Id, + ScreenNumber = screening.ScreenNumber, + Capacity = screening.Capacity, + StartsAt = screening.StartsAt + }; + await screeningRepository.Insert(newScreening); + } + } + + var response = new + { + status = "success", + data = new MovieDTO + { + Id = entity.Id, + Title = entity.Title, + Rating = entity.Rating, + Description = entity.Description, + RuntimeMins = entity.RuntimeMins, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt + } + }; + + return TypedResults.Created($"https://localhost:7239/movies/{entity.Id}", response); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task UpdateMovie(IRepository repository, int id, MoviePut model, ClaimsPrincipal user) + { + var entity = await repository.GetById(id); + + if (model.Title != null) entity.Title = model.Title; + if (model.Rating != null) entity.Rating = model.Rating; + if (model.Description != null) entity.Description = model.Description; + if (model.RuntimeMins.HasValue) entity.RuntimeMins = model.RuntimeMins.Value; + entity.UpdatedAt = DateTime.UtcNow; + + await repository.Update(entity); + + var updatedEntity = await repository.GetById(id); + + var response = new + { + status = "success", + data = new MovieDTO + { + Id = updatedEntity.Id, + Title = updatedEntity.Title, + Rating = updatedEntity.Rating, + Description = updatedEntity.Description, + RuntimeMins = updatedEntity.RuntimeMins, + CreatedAt = updatedEntity.CreatedAt, + UpdatedAt = updatedEntity.UpdatedAt + } + }; + + return TypedResults.Created($"https://localhost:7239/movies/{entity.Id}", response); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task DeleteMovie(IRepository movieRepository, IRepository screeningRepository, int id, ClaimsPrincipal user) + { + var entity = await movieRepository.GetById(id); + + await movieRepository.Delete(id); + + var screenings = await screeningRepository.Get(); + screenings = screenings.Where(s => s.MovieId == id); + foreach (var screening in screenings) + { + await screeningRepository.Delete(screening.Id); + } + + var response = new + { + status = "success", + data = new MovieDTO + { + Id = entity.Id, + Title = entity.Title, + Rating = entity.Rating, + Description = entity.Description, + RuntimeMins = entity.RuntimeMins, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt + } + }; + + return TypedResults.Ok(response); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task GetScreeningsForMovie(IRepository repository, int id, ClaimsPrincipal user) + { + var movie = await repository.GetByIdWithIncludes(id, m => m.Screenings); + + var response = new + { + status = "success", + data = new MovieWithScreenings + { + Id = movie.Id, + Screenings = movie.Screenings.Select(s => new ScreeningsForMovie + { + Id = s.Id, + ScreenNumber = s.ScreenNumber, + Capacity = s.Capacity, + StartsAt = s.StartsAt, + CreatedAt = s.CreatedAt, + UpdatedAt = s.UpdatedAt + }).ToList() + } + }; + + return TypedResults.Ok(response); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task AddScreeningToMovie(IRepository repository, int id, ScreeningPost model, ClaimsPrincipal user) + { + Screening entity = new Screening(); + entity.MovieId = id; + entity.ScreenNumber = model.ScreenNumber; + entity.Capacity = model.Capacity; + entity.StartsAt = model.StartsAt; + + await repository.Insert(entity); + + var response = new + { + status = "success", + data = new ScreeningsForMovie + { + Id = entity.Id, + ScreenNumber = entity.ScreenNumber, + Capacity = entity.Capacity, + StartsAt = entity.StartsAt, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt + } + }; + + return TypedResults.Created($"https://localhost:7239/movies/{entity.Id}/screenings", response); + } + } +} 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..3d4c0468 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs @@ -0,0 +1,28 @@ +using System.Security.Claims; + +namespace api_cinema_challenge.Helpers +{ + public static class ClaimsPrincipalHelper + { + public static string? UserId(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.NameIdentifier); + return claim?.Value; + } + public static string? Email(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.Email); + return claim?.Value; + } + + // public static string? UserId(this IIdentity identity) + // { + // if (identity != null && identity.IsAuthenticated) + // { + // // return Guid.Parse(((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value); + // return ((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value; + // } + // return null; + // } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822124636_InitialCreate.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822124636_InitialCreate.Designer.cs new file mode 100644 index 00000000..1d344122 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822124636_InitialCreate.Designer.cs @@ -0,0 +1,228 @@ +// +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("20250822124636_InitialCreate")] + partial class InitialCreate + { + /// + 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") + .HasColumnName("customer_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("customer_createdAt"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("customer_updatedAt"); + + b.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "john.doe@email.com", + Name = "John Doe", + Phone = "+4792763498", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "jane.doe@email.com", + Name = "Jane Doe", + Phone = "+4743761209", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("movie_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("movie_createdAt"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("movie_runtimeMins"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("movie_updatedAt"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Description = "The film stars Leonardo DiCaprio as a professional thief who steals information by infiltrating the subconscious of his targets. He is offered a chance to have his criminal history erased as payment for the implantation of another person's idea into a target's subconscious.", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Description = "\"The Godfather\" is based on Mario Puzo's novel of the same name. The film chronicles the life of the Corleone family, a powerful Italian-American mafia clan in New York City, focusing on the patriarch, Don Vito Corleone, and his youngest son, Michael Corleone.", + Rating = "R", + RuntimeMins = 175, + Title = "The Godfather", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("screening_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("screening_capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_createdAt"); + + b.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screening_screenNumber"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_startsAt"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_updatedAt"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 10, 1, 11, 3, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + Capacity = 150, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + MovieId = 2, + ScreenNumber = 3, + StartsAt = new DateTime(2025, 10, 1, 12, 3, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.HasOne("api_cinema_challenge.Models.Movie", "Movie") + .WithMany("Screenings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822124636_InitialCreate.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822124636_InitialCreate.cs new file mode 100644 index 00000000..dafbb81c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822124636_InitialCreate.cs @@ -0,0 +1,122 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "customers", + columns: table => new + { + customer_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + customer_name = table.Column(type: "text", nullable: false), + customer_email = table.Column(type: "text", nullable: false), + customer_phone = table.Column(type: "text", nullable: false), + customer_createdAt = table.Column(type: "timestamp with time zone", nullable: false), + customer_updatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_customers", x => x.customer_id); + }); + + migrationBuilder.CreateTable( + name: "movies", + columns: table => new + { + movie_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + movie_title = table.Column(type: "text", nullable: false), + movie_rating = table.Column(type: "text", nullable: false), + movie_description = table.Column(type: "text", nullable: false), + movie_runtimeMins = table.Column(type: "integer", nullable: false), + movie_createdAt = table.Column(type: "timestamp with time zone", nullable: false), + movie_updatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_movies", x => x.movie_id); + }); + + migrationBuilder.CreateTable( + name: "screenings", + columns: table => new + { + screening_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + movie_id = table.Column(type: "integer", nullable: false), + screening_screenNumber = table.Column(type: "integer", nullable: false), + screening_capacity = table.Column(type: "integer", nullable: false), + screening_startsAt = table.Column(type: "timestamp with time zone", nullable: false), + screening_createdAt = table.Column(type: "timestamp with time zone", nullable: false), + screening_updatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_screenings", x => x.screening_id); + table.ForeignKey( + name: "FK_screenings_movies_movie_id", + column: x => x.movie_id, + principalTable: "movies", + principalColumn: "movie_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "customers", + columns: new[] { "customer_id", "customer_createdAt", "customer_email", "customer_name", "customer_phone", "customer_updatedAt" }, + values: new object[,] + { + { 1, new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), "john.doe@email.com", "John Doe", "+4792763498", new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) }, + { 2, new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), "jane.doe@email.com", "Jane Doe", "+4743761209", new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) } + }); + + migrationBuilder.InsertData( + table: "movies", + columns: new[] { "movie_id", "movie_createdAt", "movie_description", "movie_rating", "movie_runtimeMins", "movie_title", "movie_updatedAt" }, + values: new object[,] + { + { 1, new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), "The film stars Leonardo DiCaprio as a professional thief who steals information by infiltrating the subconscious of his targets. He is offered a chance to have his criminal history erased as payment for the implantation of another person's idea into a target's subconscious.", "PG-13", 148, "Inception", new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) }, + { 2, new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), "\"The Godfather\" is based on Mario Puzo's novel of the same name. The film chronicles the life of the Corleone family, a powerful Italian-American mafia clan in New York City, focusing on the patriarch, Don Vito Corleone, and his youngest son, Michael Corleone.", "R", 175, "The Godfather", new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) } + }); + + migrationBuilder.InsertData( + table: "screenings", + columns: new[] { "screening_id", "screening_capacity", "screening_createdAt", "movie_id", "screening_screenNumber", "screening_startsAt", "screening_updatedAt" }, + values: new object[,] + { + { 1, 100, new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), 1, 5, new DateTime(2025, 10, 1, 11, 3, 0, 0, DateTimeKind.Utc), new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) }, + { 2, 150, new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), 2, 3, new DateTime(2025, 10, 1, 12, 3, 0, 0, DateTimeKind.Utc), new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) } + }); + + migrationBuilder.CreateIndex( + name: "IX_screenings_movie_id", + table: "screenings", + column: "movie_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "customers"); + + migrationBuilder.DropTable( + name: "screenings"); + + migrationBuilder.DropTable( + name: "movies"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825073316_TicketModelAdded.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825073316_TicketModelAdded.Designer.cs new file mode 100644 index 00000000..1c31ceec --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825073316_TicketModelAdded.Designer.cs @@ -0,0 +1,295 @@ +// +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("20250825073316_TicketModelAdded")] + partial class TicketModelAdded + { + /// + 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") + .HasColumnName("customer_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("customer_createdAt"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("customer_updatedAt"); + + b.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "john.doe@email.com", + Name = "John Doe", + Phone = "+4792763498", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "jane.doe@email.com", + Name = "Jane Doe", + Phone = "+4743761209", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("movie_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("movie_createdAt"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("movie_runtimeMins"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("movie_updatedAt"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Description = "The film stars Leonardo DiCaprio as a professional thief who steals information by infiltrating the subconscious of his targets. He is offered a chance to have his criminal history erased as payment for the implantation of another person's idea into a target's subconscious.", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Description = "\"The Godfather\" is based on Mario Puzo's novel of the same name. The film chronicles the life of the Corleone family, a powerful Italian-American mafia clan in New York City, focusing on the patriarch, Don Vito Corleone, and his youngest son, Michael Corleone.", + Rating = "R", + RuntimeMins = 175, + Title = "The Godfather", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("screening_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("screening_capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_createdAt"); + + b.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screening_screenNumber"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_startsAt"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_updatedAt"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 10, 1, 11, 3, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + Capacity = 150, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + MovieId = 2, + ScreenNumber = 3, + StartsAt = new DateTime(2025, 10, 1, 12, 3, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ticket_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ticket_createdAt"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("ScreeningId") + .HasColumnType("integer") + .HasColumnName("screening_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ticket_updatedAt"); + + b.Property("numSeats") + .HasColumnType("integer") + .HasColumnName("ticket_numSeats"); + + 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/20250825073316_TicketModelAdded.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825073316_TicketModelAdded.cs new file mode 100644 index 00000000..b91a10da --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825073316_TicketModelAdded.cs @@ -0,0 +1,62 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class TicketModelAdded : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "tickets", + columns: table => new + { + ticket_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + customer_id = table.Column(type: "integer", nullable: false), + screening_id = table.Column(type: "integer", nullable: false), + ticket_numSeats = table.Column(type: "integer", nullable: false), + ticket_createdAt = table.Column(type: "timestamp with time zone", nullable: false), + ticket_updatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tickets", x => x.ticket_id); + table.ForeignKey( + name: "FK_tickets_customers_customer_id", + column: x => x.customer_id, + principalTable: "customers", + principalColumn: "customer_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_tickets_screenings_screening_id", + column: x => x.screening_id, + principalTable: "screenings", + principalColumn: "screening_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_tickets_customer_id", + table: "tickets", + column: "customer_id"); + + migrationBuilder.CreateIndex( + name: "IX_tickets_screening_id", + table: "tickets", + column: "screening_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "tickets"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825090500_AddedUsers.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825090500_AddedUsers.Designer.cs new file mode 100644 index 00000000..bcde6d0e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825090500_AddedUsers.Designer.cs @@ -0,0 +1,545 @@ +// +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("20250825090500_AddedUsers")] + partial class AddedUsers + { + /// + 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.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") + .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") + .HasColumnName("customer_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("customer_createdAt"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("customer_updatedAt"); + + b.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "john.doe@email.com", + Name = "John Doe", + Phone = "+4792763498", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "jane.doe@email.com", + Name = "Jane Doe", + Phone = "+4743761209", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("movie_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("movie_createdAt"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("movie_runtimeMins"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("movie_updatedAt"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Description = "The film stars Leonardo DiCaprio as a professional thief who steals information by infiltrating the subconscious of his targets. He is offered a chance to have his criminal history erased as payment for the implantation of another person's idea into a target's subconscious.", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Description = "\"The Godfather\" is based on Mario Puzo's novel of the same name. The film chronicles the life of the Corleone family, a powerful Italian-American mafia clan in New York City, focusing on the patriarch, Don Vito Corleone, and his youngest son, Michael Corleone.", + Rating = "R", + RuntimeMins = 175, + Title = "The Godfather", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("screening_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("screening_capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_createdAt"); + + b.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screening_screenNumber"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_startsAt"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_updatedAt"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 10, 1, 11, 3, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + Capacity = 150, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + MovieId = 2, + ScreenNumber = 3, + StartsAt = new DateTime(2025, 10, 1, 12, 3, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ticket_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ticket_createdAt"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("ScreeningId") + .HasColumnType("integer") + .HasColumnName("screening_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ticket_updatedAt"); + + b.Property("numSeats") + .HasColumnType("integer") + .HasColumnName("ticket_numSeats"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + 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(); + }); + + 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") + .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/20250825090500_AddedUsers.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825090500_AddedUsers.cs new file mode 100644 index 00000000..98634b32 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825090500_AddedUsers.cs @@ -0,0 +1,224 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class AddedUsers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + 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: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = 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_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + 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: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + RoleId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_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_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + 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: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825104150_IdentityUserContext.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825104150_IdentityUserContext.Designer.cs new file mode 100644 index 00000000..b344d44f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825104150_IdentityUserContext.Designer.cs @@ -0,0 +1,455 @@ +// +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("20250825104150_IdentityUserContext")] + partial class IdentityUserContext + { + /// + 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") + .HasColumnName("customer_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("customer_createdAt"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("customer_updatedAt"); + + b.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "john.doe@email.com", + Name = "John Doe", + Phone = "+4792763498", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "jane.doe@email.com", + Name = "Jane Doe", + Phone = "+4743761209", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("movie_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("movie_createdAt"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("movie_runtimeMins"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("movie_updatedAt"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Description = "The film stars Leonardo DiCaprio as a professional thief who steals information by infiltrating the subconscious of his targets. He is offered a chance to have his criminal history erased as payment for the implantation of another person's idea into a target's subconscious.", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Description = "\"The Godfather\" is based on Mario Puzo's novel of the same name. The film chronicles the life of the Corleone family, a powerful Italian-American mafia clan in New York City, focusing on the patriarch, Don Vito Corleone, and his youngest son, Michael Corleone.", + Rating = "R", + RuntimeMins = 175, + Title = "The Godfather", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("screening_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("screening_capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_createdAt"); + + b.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screening_screenNumber"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_startsAt"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_updatedAt"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 10, 1, 11, 3, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + Capacity = 150, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + MovieId = 2, + ScreenNumber = 3, + StartsAt = new DateTime(2025, 10, 1, 12, 3, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ticket_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ticket_createdAt"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("ScreeningId") + .HasColumnType("integer") + .HasColumnName("screening_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ticket_updatedAt"); + + b.Property("numSeats") + .HasColumnType("integer") + .HasColumnName("ticket_numSeats"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("tickets"); + }); + + 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(); + }); + + 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/20250825104150_IdentityUserContext.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825104150_IdentityUserContext.cs new file mode 100644 index 00000000..7bc9c48a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825104150_IdentityUserContext.cs @@ -0,0 +1,103 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class IdentityUserContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true), + RoleId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + RoleId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + } + } +} 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..689999c0 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,452 @@ +// +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") + .HasColumnName("customer_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("customer_createdAt"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("customer_phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("customer_updatedAt"); + + b.HasKey("Id"); + + b.ToTable("customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "john.doe@email.com", + Name = "John Doe", + Phone = "+4792763498", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "jane.doe@email.com", + Name = "Jane Doe", + Phone = "+4743761209", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("movie_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("movie_createdAt"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("movie_runtimeMins"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("movie_title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("movie_updatedAt"); + + b.HasKey("Id"); + + b.ToTable("movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Description = "The film stars Leonardo DiCaprio as a professional thief who steals information by infiltrating the subconscious of his targets. He is offered a chance to have his criminal history erased as payment for the implantation of another person's idea into a target's subconscious.", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + Description = "\"The Godfather\" is based on Mario Puzo's novel of the same name. The film chronicles the life of the Corleone family, a powerful Italian-American mafia clan in New York City, focusing on the patriarch, Don Vito Corleone, and his youngest son, Michael Corleone.", + Rating = "R", + RuntimeMins = 175, + Title = "The Godfather", + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("screening_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("screening_capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_createdAt"); + + b.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screening_screenNumber"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_startsAt"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("screening_updatedAt"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 10, 1, 11, 3, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + Capacity = 150, + CreatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc), + MovieId = 2, + ScreenNumber = 3, + StartsAt = new DateTime(2025, 10, 1, 12, 3, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 15, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ticket_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ticket_createdAt"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("ScreeningId") + .HasColumnType("integer") + .HasColumnName("screening_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ticket_updatedAt"); + + b.Property("numSeats") + .HasColumnType("integer") + .HasColumnName("ticket_numSeats"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("tickets"); + }); + + 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(); + }); + + 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..6b59b648 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs @@ -0,0 +1,10 @@ +using api_cinema_challenge.Enums; +using Microsoft.AspNetCore.Identity; + +namespace api_cinema_challenge.Models +{ + public class ApplicationUser : IdentityUser + { + public Role Role { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs new file mode 100644 index 00000000..0c912ca9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("customers")] + public class Customer + { + [Key] + [Column("customer_id")] + public int Id { get; set; } + + [Column("customer_name")] + public string Name { get; set; } = string.Empty; + + [EmailAddress] + [Column("customer_email")] + public string Email { get; set; } = string.Empty; + + [Column("customer_phone")] + public string Phone { get; set; } = string.Empty; + + [Column("customer_createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Column("customer_updatedAt")] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + [Column("tickets")] + 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 new file mode 100644 index 00000000..f5916ad4 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("movies")] + public class Movie + { + [Key] + [Column("movie_id")] + public int Id { get; set; } + + [Column("movie_title")] + public string Title { get; set; } = string.Empty; + + [Column("movie_rating")] + public string Rating { get; set; } = string.Empty; + + [Column("movie_description")] + public string Description { get; set; } = string.Empty; + + [Column("movie_runtimeMins")] + public int RuntimeMins { get; set; } + + [Column("movie_createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Column("movie_updatedAt")] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + [Column("screenings")] + 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 new file mode 100644 index 00000000..318eca25 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("screenings")] + public class Screening + { + [Key] + [Column("screening_id")] + public int Id { get; set; } + + [ForeignKey("Movie")] + [Column("movie_id")] + public int MovieId { get; set; } + + [Column("movie")] + public Movie Movie { get; set; } + + [Column("screening_screenNumber")] + public int ScreenNumber { get; set; } + + [Column("screening_capacity")] + public int Capacity { get; set; } + + [Column("screening_startsAt")] + public DateTime StartsAt { get; set; } + + [Column("screening_createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Column("screening_updatedAt")] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + [Column("tickets")] + public ICollection Tickets { get; set; } = new List(); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs new file mode 100644 index 00000000..5455b394 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("tickets")] + public class Ticket + { + [Key] + [Column("ticket_id")] + public int Id { get; set; } + + [ForeignKey("Customer")] + [Column("customer_id")] + public int CustomerId { get; set; } + + [Column("customer")] + public Customer Customer { get; set; } + + [ForeignKey("Screening")] + [Column("screening_id")] + public int ScreeningId { get; set; } + + [Column("screening")] + public Screening Screening { get; set; } + + [Column("ticket_numSeats")] + public int numSeats { get; set; } + + [Column("ticket_createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Column("ticket_updatedAt")] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..e2df36ab 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,12 +1,111 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using System.Text; +using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); + +builder.Services.AddSwaggerGen(option => +{ + option.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); + option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter a valid token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + option.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); +}); + +builder.Services.AddProblemDetails(); +//builder.Services.AddApiVersioning(); // install-package Microsoft.AspNetCore.Mvc.Versioning +builder.Services.AddRouting(options => options.LowercaseUrls = true); + builder.Services.AddDbContext(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +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) + ), + }; + }); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -17,4 +116,13 @@ } app.UseHttpsRedirection(); +app.UseStatusCodePages(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.ConfigureCustomerEndpoints(); +app.ConfigureMovieEndpoints(); + +app.MapControllers(); app.Run(); 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..0ed98079 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs @@ -0,0 +1,18 @@ +using System.Linq.Expressions; + +namespace api_cinema_challenge.Repository +{ + public interface IRepository + { + Task> Get(); + Task GetById(int id); + Task Insert(T entity); + Task Update(T entity); + Task Delete(object id); + + Task> GetWithIncludes(params Expression>[] includes); + Task GetByIdWithIncludes(int id, params Expression>[] includes); + Task> GetWithCustomIncludes(Func, IQueryable> includeQuery); + Task GetByIdWithCustomIncludes(int id, Func, IQueryable> includeQuery); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs new file mode 100644 index 00000000..072f661d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs @@ -0,0 +1,83 @@ +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace api_cinema_challenge.Repository +{ + public class Repository : IRepository where T : class + { + private CinemaContext _db; + private DbSet _table = null!; + public Repository(CinemaContext db) + { + _db = db; + _table = _db.Set(); + } + + public async Task> Get() + { + return await _table.ToListAsync(); + } + + public async Task Insert(T entity) + { + await _table.AddAsync(entity); + await _db.SaveChangesAsync(); + return entity; + } + + public async Task Update(T entity) + { + _table.Attach(entity); + _db.Entry(entity).State = EntityState.Modified; + await _db.SaveChangesAsync(); + return entity; + } + + public async Task Delete(object id) + { + T entity = await _table.FindAsync(id); + _table.Remove(entity); + await _db.SaveChangesAsync(); + return entity; + } + + public async Task GetById(int id) + { + return await _table.FindAsync(id); + } + + public async Task> GetWithIncludes(params Expression>[] includes) + { + IQueryable query = _table; + foreach (var include in includes) + { + query = query.Include(include); + } + return await query.ToListAsync(); + } + + public async Task GetByIdWithIncludes(int id, params Expression>[] includes) + { + IQueryable query = _table; + + foreach (var include in includes) + { + query = query.Include(include); + } + + return await query.FirstOrDefaultAsync(e => EF.Property(e, "Id") == id); + } + + public async Task> GetWithCustomIncludes(Func, IQueryable> includeQuery) + { + IQueryable query = includeQuery(_table); + return await query.ToListAsync(); + } + public async Task GetByIdWithCustomIncludes(int id, Func, IQueryable> includeQuery) + { + IQueryable query = includeQuery(_table); + return await query.FirstOrDefaultAsync(e => EF.Property(e, "Id") == id); + } + } +} 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..740071cf --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs @@ -0,0 +1,82 @@ +using api_cinema_challenge.Models; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace api_cinema_challenge.Services +{ + public class TokenService + { + private const int ExpirationMinutes = 60; + private readonly ILogger _logger; + public TokenService(ILogger logger) + { + _logger = logger; + } + + public string CreateToken(ApplicationUser user) + { + + var expiration = DateTime.UtcNow.AddMinutes(ExpirationMinutes); + var token = CreateJwtToken( + CreateClaims(user), + CreateSigningCredentials(), + expiration + ); + var tokenHandler = new JwtSecurityTokenHandler(); + + _logger.LogInformation("JWT Token created"); + + return tokenHandler.WriteToken(token); + } + + private JwtSecurityToken CreateJwtToken(List claims, SigningCredentials credentials, + DateTime expiration) => + new( + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidIssuer"], + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidAudience"], + claims, + expires: expiration, + signingCredentials: credentials + ); + + private List CreateClaims(ApplicationUser user) + { + var jwtSub = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["JwtRegisteredClaimNamesSub"]; + + try + { + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, jwtSub), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()) + }; + + return claims; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + private SigningCredentials CreateSigningCredentials() + { + var symmetricSecurityKey = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["SymmetricSecurityKey"]; + + return new SigningCredentials( + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + SecurityAlgorithms.HmacSha256 + ); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj index 11e5c66b..103e514f 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,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