From c69963898ebbed0daa20758f800e260ead18bf38 Mon Sep 17 00:00:00 2001 From: Isabell Date: Mon, 25 Aug 2025 14:04:17 +0200 Subject: [PATCH 1/3] Core done --- .../Controllers/UsersController.cs | 101 +++++ .../api-cinema-challenge/DTOs/CustomerPost.cs | 9 + .../api-cinema-challenge/DTOs/CustomerPut.cs | 9 + .../api-cinema-challenge/DTOs/MoviePost.cs | 10 + .../api-cinema-challenge/DTOs/MoviePut.cs | 10 + .../api-cinema-challenge/DTOs/ScreeningGet.cs | 11 + .../DTOs/ScreeningPost.cs | 9 + .../Data/CinemaContext.cs | 96 ++++- .../DataTransfer/Requests/AuthRequest.cs | 12 + .../Requests/RegistrationRequest.cs | 19 + .../DataTransfer/Response/AuthResponse.cs | 9 + .../Endpoints/CustomerEndpoints.cs | 79 ++++ .../Endpoints/MovieEndpoints.cs | 97 +++++ .../api-cinema-challenge/Enums/Role.cs | 8 + .../Helpers/ClaimsPrincipalHelper.cs | 30 ++ .../20250822123432_seond.Designer.cs | 200 +++++++++ .../Migrations/20250822123432_seond.cs | 122 ++++++ .../20250825064900_first.Designer.cs | 219 ++++++++++ .../Migrations/20250825064900_first.cs | 39 ++ .../20250825081039_third.Designer.cs | 219 ++++++++++ .../Migrations/20250825081039_third.cs | 22 + .../20250825114610_foruth.Designer.cs | 379 ++++++++++++++++++ .../Migrations/20250825114610_foruth.cs | 140 +++++++ .../Migrations/CinemaContextModelSnapshot.cs | 376 +++++++++++++++++ .../Models/ApplicationUser.cs | 11 + .../api-cinema-challenge/Models/Customer.cs | 12 + .../api-cinema-challenge/Models/Movie.cs | 13 + .../api-cinema-challenge/Models/Screening.cs | 13 + .../api-cinema-challenge/Models/Ticket.cs | 12 + .../api-cinema-challenge/Program.cs | 125 +++++- .../Repository/IRepository.cs | 27 ++ .../Repository/Repository.cs | 93 +++++ .../Services/TokenService.cs | 80 ++++ .../api-cinema-challenge.csproj | 20 +- .../appsettings.example.json | 12 - 35 files changed, 2603 insertions(+), 40 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Controllers/UsersController.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPost.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Enums/Role.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Customer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Movie.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Screening.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs delete mode 100644 api-cinema-challenge/api-cinema-challenge/appsettings.example.json diff --git a/api-cinema-challenge/api-cinema-challenge/Controllers/UsersController.cs b/api-cinema-challenge/api-cinema-challenge/Controllers/UsersController.cs new file mode 100644 index 00000000..394239be --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Controllers/UsersController.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using workshop.webapi.Services; +using workshop.webapi.DataTransfer.Response; +using workshop.webapi.DataTransfer.Requests; +using api_cinema_challenge.Enums; + +namespace workshop.webapi.Controllers +{ + + [ApiController] + [Route("/api/[controller]")] + public class UsersController : ControllerBase + { + private readonly UserManager _userManager; + private readonly CinemaContext _context; + private readonly TokenService _tokenService; + + public UsersController(UserManager userManager, CinemaContext context, + TokenService tokenService, ILogger logger) + { + _userManager = userManager; + _context = context; + _tokenService = tokenService; + } + + + [HttpPost] + [Route("register")] + public async Task Register(RegistrationRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var result = await _userManager.CreateAsync( + new ApplicationUser { UserName = request.Username, Email = request.Email, Role = request.Role }, + request.Password! + ); + + if (result.Succeeded) + { + request.Password = ""; + return CreatedAtAction(nameof(Register), new { email = request.Email, role = Role.User }, request); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(error.Code, error.Description); + } + + return BadRequest(ModelState); + } + + + [HttpPost] + [Route("login")] + public async Task> Authenticate([FromBody] AuthRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var managedUser = await _userManager.FindByEmailAsync(request.Email!); + + if (managedUser == null) + { + return BadRequest("Bad credentials"); + } + + var isPasswordValid = await _userManager.CheckPasswordAsync(managedUser, request.Password!); + + if (!isPasswordValid) + { + return BadRequest("Bad credentials"); + } + + var userInDb = _context.Users.FirstOrDefault(u => u.Email == request.Email); + + if (userInDb is null) + { + return Unauthorized(); + } + + var accessToken = _tokenService.CreateToken(userInDb); + await _context.SaveChangesAsync(); + + return Ok(new AuthResponse + { + Username = userInDb.UserName, + Email = userInDb.Email, + Token = accessToken, + }); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPost.cs new file mode 100644 index 00000000..7b36bb48 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPost.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs +{ + public class CustomerPost + { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs new file mode 100644 index 00000000..f02ae3ee --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPut.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs +{ + public class CustomerPut + { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs new file mode 100644 index 00000000..a765657e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs +{ + public class MoviePost + { + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs new file mode 100644 index 00000000..c8005d5e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePut.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs +{ + public class MoviePut + { + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs new file mode 100644 index 00000000..9e12cfde --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs @@ -0,0 +1,11 @@ +namespace api_cinema_challenge.DTOs +{ + public class ScreeningGet + { + public int Id { get; set; } + public int MovieId { get; set; } + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs new file mode 100644 index 00000000..f58f4eac --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs +{ + public class ScreeningPost + { + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index ad4fe854..6c233586 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,26 +1,100 @@ -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json.Linq; +using api_cinema_challenge.Models; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace api_cinema_challenge.Data { - public class CinemaContext : DbContext + public class CinemaContext : IdentityUserContext { private string _connectionString; public CinemaContext(DbContextOptions options) : base(options) { - var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - _connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString")!; - this.Database.EnsureCreated(); - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseNpgsql(_connectionString); + } protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity().HasData( + new Customer + { + Id = 1, + Name = "Isabell Tran", + Email = "isabell@experis.com", + Phone = "12345678", + CreatedAt = new DateTime(2025, 08, 22, 10, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 22, 10, 00, 00, DateTimeKind.Utc) + } + ); + modelBuilder.Entity().HasData( + new Customer + { + Id = 2, + Name = "Marie Hansen", + Email = "Marie@experis.com", + Phone = "98989898", + CreatedAt = new DateTime(2025, 08, 23, 10, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 23, 10, 00, 00, DateTimeKind.Utc) + } + ); + + modelBuilder.Entity().HasData( + new Movie + { + Id = 1, + Title = "Inception", + Rating = "PG-13", + Description = "A mind-bending thriller", + RuntimeMins = 148, + CreatedAt = new DateTime(2025, 08, 22, 15, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 22, 15, 00, 00, DateTimeKind.Utc) + } + ); + + modelBuilder.Entity().HasData( + new Movie + { + Id = 2, + Title = "F1", + Rating = "PG-13", + Description = "Action", + RuntimeMins = 155, + CreatedAt = new DateTime(2025, 08, 22, 15, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 22, 15, 00, 00, DateTimeKind.Utc) + } + ); + + modelBuilder.Entity().HasData( + new Screening + { + Id = 1, + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 08, 22, 14, 00, 00, DateTimeKind.Utc), + Capacity = 100, + CreatedAt = new DateTime(2025, 08, 22, 14, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 22, 14, 00, 00, DateTimeKind.Utc) + } + ); + + modelBuilder.Entity().HasData( + new Ticket + { + Id = 1, + CustomerId = 1, + ScreeningId = 1, + NumSeats = 2, + CreatedAt = new DateTime(2025, 08, 22, 13, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 08, 22, 13, 00, 00, DateTimeKind.Utc) + } + ); + base.OnModelCreating(modelBuilder); + } + + public DbSet Customers { get; set; } + public DbSet Movies { get; set; } + public DbSet Screenings { get; set; } + public DbSet Tickets { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs new file mode 100644 index 00000000..c5f0c2e9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs @@ -0,0 +1,12 @@ +namespace workshop.webapi.DataTransfer.Requests; + +public class AuthRequest +{ + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() + { + return true; + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs new file mode 100644 index 00000000..7f706f29 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs @@ -0,0 +1,19 @@ + + +using api_cinema_challenge.Enums; +using System.ComponentModel.DataAnnotations; + + +public class RegistrationRequest +{ + [Required] + public string? Email { get; set; } + + [Required] + public string? Username { get { return this.Email; } set { } } + + [Required] + public string? Password { get; set; } + + public Role Role { get; set; } = Role.User; +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs new file mode 100644 index 00000000..f108e151 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs @@ -0,0 +1,9 @@ +namespace workshop.webapi.DataTransfer.Response; + + +public class AuthResponse +{ + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs new file mode 100644 index 00000000..dbe69926 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs @@ -0,0 +1,79 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class CustomerEndpoints + { + public static void ConfigureCustomerEndpoint(this WebApplication app) + { + var customerGroup = app.MapGroup("customer").RequireAuthorization(); + + customerGroup.MapPost("/", AddCustomer); + customerGroup.MapGet("/", GetCustomers); + customerGroup.MapPut("/{id}", UpdateCustomer); + customerGroup.MapDelete("/{id}", DeleteCustomer); + + } + + // Adding Authorize + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task AddCustomer(IRepository repository, CustomerPost customer) + { + var results = await repository.GetCustomers(); + if (results.Any(x => x.Name.Equals(customer.Name, StringComparison.OrdinalIgnoreCase))) + { + return Results.BadRequest("Customer with provided name already exists"); + } + Customer entity = new Customer(); + entity.Name = customer.Name; + entity.Email = customer.Email; + entity.Phone = customer.Phone; + entity.CreatedAt = DateTime.UtcNow; + entity.UpdatedAt = DateTime.UtcNow; + await repository.AddCustomer(entity); + + return TypedResults.Created($"https://localhost:7195/customer/{entity.Id}", new { Name = customer.Name, Email = customer.Email, Phone = customer.Phone, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }); + + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task GetCustomers(IRepository repository) + { + var results = await repository.GetCustomers(); + return TypedResults.Ok(results); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task UpdateCustomer(IRepository repository, int id, CustomerPut customer) + { + var entity = await repository.GetCustomerId(id); + + if (customer.Name != null) entity.Name = customer.Name; + if (customer.Email != null) entity.Email = customer.Email; + if (customer.Phone != null) entity.Phone = customer.Phone; + entity.UpdatedAt = DateTime.UtcNow; + entity.CreatedAt = DateTime.UtcNow; + + await repository.UpdateCustomer(id, entity); + + return TypedResults.Created($"https://localhost:7195/customer/{entity.Id}", new { Name = customer.Name, Email = customer.Email, Phone = customer.Phone, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task DeleteCustomer(IRepository repository, int id) + { + var entity = await repository.DeleteCustomer(id); + return TypedResults.Ok(entity); + } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs new file mode 100644 index 00000000..2d05d666 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs @@ -0,0 +1,97 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class MovieEndpoints + { + public static void ConfigureMovieEndpoint(this WebApplication app) + { + var movieGroup = app.MapGroup("movie").RequireAuthorization(); + + movieGroup.MapPost("/", AddMovie); + movieGroup.MapGet("/", GetMovies); + movieGroup.MapPut("/{id}", UpdateMovie); + movieGroup.MapDelete("/{id}", DeleteMovie); + movieGroup.MapPost("/{id}/screenings", AddScreening); + movieGroup.MapGet("/{id}/screenings", GetScreenings); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task AddMovie(IRepository repository, MoviePost movie) + { + Movie entity = new Movie(); + entity.Title = movie.Title; + entity.Rating = movie.Rating; + entity.Description = movie.Description; + entity.RuntimeMins = movie.RuntimeMins; + entity.CreatedAt = DateTime.UtcNow; + entity.UpdatedAt = DateTime.UtcNow; + var results = await repository.AddMovie(entity); + return TypedResults.Created($"https://localhost:7195/movie/{entity.Id}", new { Title = movie.Title, Rating = movie.Rating, Description = movie.Description, RuntimeMins = movie.RuntimeMins, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }); + + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetMovies(IRepository repository) + { + var results = await repository.GetMovies(); + return TypedResults.Ok(results); + } + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task UpdateMovie(IRepository repository, int id, MoviePut movie) + { + var entity = await repository.GetMovieId(id); + + if (movie.Title != null) entity.Title = movie.Title; + if (movie.Rating != null) entity.Rating = movie.Rating; + if (movie.Description != null) entity.Description = movie.Description; + if (movie.RuntimeMins != null) entity.RuntimeMins = movie.RuntimeMins; + entity.UpdatedAt = DateTime.UtcNow; + entity.CreatedAt = DateTime.UtcNow; + + await repository.UpdateMovie(id, entity); + return TypedResults.Created($"https://localhost:7195/movie/{entity.Id}", new { Title = movie.Title, Rating = movie.Rating, Description = movie.Description, RuntimeMins = movie.RuntimeMins, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }); + + } + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task DeleteMovie(IRepository repository, int id) + { + var results = await repository.DeleteMovie(id); + return TypedResults.Ok(results); + } + + // Screenings + + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task AddScreening(IRepository repository, ScreeningPost screening, int movieId) + { + Screening entity = new Screening(); + entity.ScreenNumber = screening.ScreenNumber; + entity.Capacity = screening.Capacity; + entity.StartsAt = screening.StartsAt; + entity.CreatedAt = DateTime.UtcNow; + entity.UpdatedAt = DateTime.UtcNow; + var results = await repository.AddScreening(entity); + return TypedResults.Created($"https://localhost:7195/movie/{movieId}/screenings", new { Id = entity.Id, ScreenNumber = screening.ScreenNumber, Capacity = screening.Capacity, StartsAt = screening.StartsAt, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }); + + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetScreenings(IRepository repository, int movieId) + { + List screens = new List(); + var results = await repository.GetScreenings(); + foreach (Screening screen in results) + { + if (movieId == screen.MovieId) + { + screens.Add(new ScreeningGet() {Id = screen.Id, ScreenNumber = screen.ScreenNumber, Capacity = screen.Capacity, StartsAt = screen.StartsAt }); + } + } + return TypedResults.Ok(screens); + } + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs new file mode 100644 index 00000000..551a6178 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.Enums +{ + public enum Role + { + Admin, + User + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs new file mode 100644 index 00000000..e394077e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelper.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; +using System.Security.Claims; + +namespace workshop.webapi +{ + public static class ClaimsPrincipalHelper + { + public static string? UserId(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.NameIdentifier); + return claim?.Value; + } + public static string? Email(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.Email); + return claim?.Value; + } + + // public static string? UserId(this IIdentity identity) + // { + // if (identity != null && identity.IsAuthenticated) + // { + // // return Guid.Parse(((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value); + // return ((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value; + // } + // return null; + // } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.Designer.cs new file mode 100644 index 00000000..bb798009 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.Designer.cs @@ -0,0 +1,200 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + [Migration("20250822123432_seond")] + partial class seond + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "isabell@experis.com", + Name = "Isabell Tran", + Phone = "12345678", + UpdatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "A mind-bending thriller", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), + CustomerId = 1, + NumSeats = 2, + ScreeningId = 1, + UpdatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.cs new file mode 100644 index 00000000..a7793255 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250822123432_seond.cs @@ -0,0 +1,122 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class seond : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Movies", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "text", nullable: false), + Rating = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + RuntimeMins = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Movies", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Screenings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MovieId = table.Column(type: "integer", nullable: false), + ScreenNumber = table.Column(type: "integer", nullable: false), + StartsAt = table.Column(type: "timestamp with time zone", nullable: false), + Capacity = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Screenings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CustomerId = table.Column(type: "integer", nullable: false), + ScreeningId = table.Column(type: "integer", nullable: false), + NumSeats = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => x.Id); + }); + + migrationBuilder.InsertData( + table: "Customers", + columns: new[] { "Id", "CreatedAt", "Email", "Name", "Phone", "UpdatedAt" }, + values: new object[] { 1, new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), "isabell@experis.com", "Isabell Tran", "12345678", new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) }); + + migrationBuilder.InsertData( + table: "Movies", + columns: new[] { "Id", "CreatedAt", "Description", "Rating", "RuntimeMins", "Title", "UpdatedAt" }, + values: new object[] { 1, new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), "A mind-bending thriller", "PG-13", 148, "Inception", new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) }); + + migrationBuilder.InsertData( + table: "Screenings", + columns: new[] { "Id", "Capacity", "CreatedAt", "MovieId", "ScreenNumber", "StartsAt", "UpdatedAt" }, + values: new object[] { 1, 100, new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), 1, 5, new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) }); + + migrationBuilder.InsertData( + table: "Tickets", + columns: new[] { "Id", "CreatedAt", "CustomerId", "NumSeats", "ScreeningId", "UpdatedAt" }, + values: new object[] { 1, new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), 1, 2, 1, new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Movies"); + + migrationBuilder.DropTable( + name: "Screenings"); + + migrationBuilder.DropTable( + name: "Tickets"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.Designer.cs new file mode 100644 index 00000000..27e669f4 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.Designer.cs @@ -0,0 +1,219 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + [Migration("20250825064900_first")] + partial class first + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "isabell@experis.com", + Name = "Isabell Tran", + Phone = "12345678", + UpdatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "Marie@experis.com", + Name = "Marie Hansen", + Phone = "98989898", + UpdatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "A mind-bending thriller", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "Action", + Rating = "PG-13", + RuntimeMins = 155, + Title = "F1", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), + CustomerId = 1, + NumSeats = 2, + ScreeningId = 1, + UpdatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.cs new file mode 100644 index 00000000..5b99799b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825064900_first.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class first : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Customers", + columns: new[] { "Id", "CreatedAt", "Email", "Name", "Phone", "UpdatedAt" }, + values: new object[] { 2, new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc), "Marie@experis.com", "Marie Hansen", "98989898", new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc) }); + + migrationBuilder.InsertData( + table: "Movies", + columns: new[] { "Id", "CreatedAt", "Description", "Rating", "RuntimeMins", "Title", "UpdatedAt" }, + values: new object[] { 2, new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), "Action", "PG-13", 155, "F1", new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Customers", + keyColumn: "Id", + keyValue: 2); + + migrationBuilder.DeleteData( + table: "Movies", + keyColumn: "Id", + keyValue: 2); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.Designer.cs new file mode 100644 index 00000000..e02c85aa --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.Designer.cs @@ -0,0 +1,219 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + [Migration("20250825081039_third")] + partial class third + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "isabell@experis.com", + Name = "Isabell Tran", + Phone = "12345678", + UpdatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "Marie@experis.com", + Name = "Marie Hansen", + Phone = "98989898", + UpdatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "A mind-bending thriller", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "Action", + Rating = "PG-13", + RuntimeMins = 155, + Title = "F1", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), + CustomerId = 1, + NumSeats = 2, + ScreeningId = 1, + UpdatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.cs new file mode 100644 index 00000000..db92d939 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825081039_third.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class third : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.Designer.cs new file mode 100644 index 00000000..60a84007 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.Designer.cs @@ -0,0 +1,379 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + [Migration("20250825114610_foruth")] + partial class foruth + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "isabell@experis.com", + Name = "Isabell Tran", + Phone = "12345678", + UpdatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "Marie@experis.com", + Name = "Marie Hansen", + Phone = "98989898", + UpdatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "A mind-bending thriller", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "Action", + Rating = "PG-13", + RuntimeMins = 155, + Title = "F1", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), + CustomerId = 1, + NumSeats = 2, + ScreeningId = 1, + UpdatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.cs new file mode 100644 index 00000000..0756fc8d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250825114610_foruth.cs @@ -0,0 +1,140 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class foruth : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Role = table.Column(type: "integer", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs new file mode 100644 index 00000000..521da52f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,376 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + partial class CinemaContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "isabell@experis.com", + Name = "Isabell Tran", + Phone = "12345678", + UpdatedAt = new DateTime(2025, 8, 22, 10, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc), + Email = "Marie@experis.com", + Name = "Marie Hansen", + Phone = "98989898", + UpdatedAt = new DateTime(2025, 8, 23, 10, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "A mind-bending thriller", + Rating = "PG-13", + RuntimeMins = 148, + Title = "Inception", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + CreatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc), + Description = "Action", + Rating = "PG-13", + RuntimeMins = 155, + Title = "F1", + UpdatedAt = new DateTime(2025, 8, 22, 15, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 100, + CreatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + MovieId = 1, + ScreenNumber = 5, + StartsAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 8, 22, 14, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc), + CustomerId = 1, + NumSeats = 2, + ScreeningId = 1, + UpdatedAt = new DateTime(2025, 8, 22, 13, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs new file mode 100644 index 00000000..cbee7397 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs @@ -0,0 +1,11 @@ +using api_cinema_challenge.Enums; +using Microsoft.AspNetCore.Identity; +using System.Data; + +namespace api_cinema_challenge.Models +{ + public class ApplicationUser : IdentityUser + { + public Role Role { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs new file mode 100644 index 00000000..cb2a838e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.Models +{ + public class Customer + { + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs new file mode 100644 index 00000000..34970903 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,13 @@ +namespace api_cinema_challenge.Models +{ + public class Movie + { + public int Id { get; set; } + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs new file mode 100644 index 00000000..17fd44d6 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,13 @@ +namespace api_cinema_challenge.Models +{ + public class Screening + { + public int Id { get; set; } + public int MovieId { get; set; } + public int ScreenNumber { get; set; } + public DateTime StartsAt { get; set; } + public int Capacity { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs new file mode 100644 index 00000000..c4485faf --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.Models +{ + public class Ticket + { + public int Id { get; set; } + public int CustomerId { get; set; } + public int ScreeningId { get; set; } + public int NumSeats { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..23e906e5 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,15 +1,121 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using System.Diagnostics; +using System.Text; +using System.Text.Json.Serialization; +using workshop.webapi.Services; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. +// Add services +builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -builder.Services.AddDbContext(); +builder.Services.AddSwaggerGen(option => +{ + option.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); + option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter a valid token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + option.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); +}); + +builder.Services.AddProblemDetails(); +builder.Services.AddApiVersioning(); +builder.Services.AddRouting(options => options.LowercaseUrls = true); +builder.Services.AddDbContext(opt => +{ + opt.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); + opt.LogTo(message => Debug.WriteLine(message)); + +}); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Support string to enum conversions +builder.Services.AddControllers().AddJsonOptions(opt => +{ + opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); + + +// Specify identity requirements +// Must be added before .AddAuthentication otherwise a 404 is thrown on authorized endpoints +builder.Services + .AddIdentity(options => + { + options.SignIn.RequireConfirmedAccount = false; + options.User.RequireUniqueEmail = true; + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + }) + .AddRoles() + .AddEntityFrameworkStores(); + + +// These will eventually be moved to a secrets file, but for alpha development appsettings is fine +var validIssuer = builder.Configuration.GetValue("JwtTokenSettings:ValidIssuer"); +var validAudience = builder.Configuration.GetValue("JwtTokenSettings:ValidAudience"); +var symmetricSecurityKey = builder.Configuration.GetValue("JwtTokenSettings:SymmetricSecurityKey"); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +}) + .AddJwtBearer(options => + { + options.IncludeErrorDetails = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ClockSkew = TimeSpan.Zero, + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = validIssuer, + ValidAudience = validAudience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + }; + }); + +// Build the app var app = builder.Build(); -// Configure the HTTP request pipeline. + +// Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.UseSwagger(); @@ -17,4 +123,13 @@ } app.UseHttpsRedirection(); -app.Run(); +app.UseStatusCodePages(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.ConfigureCustomerEndpoint(); +app.ConfigureMovieEndpoint(); + +app.MapControllers(); +app.Run(); \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs new file mode 100644 index 00000000..35a60a5e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs @@ -0,0 +1,27 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository +{ + public interface IRepository + { + // Customer + Task AddCustomer(Customer customer); + Task> GetCustomers(); + Task GetCustomerId(int id); + Task UpdateCustomer(int id, Customer customer); + Task DeleteCustomer(int id); + + // Movie + Task AddMovie(Movie movie); + Task> GetMovies(); + Task GetMovieId(int id); + Task UpdateMovie(int id, Movie movie); + Task DeleteMovie(int id); + + // Screening + Task AddScreening(Screening screening); + Task> GetScreenings(); + } +} + + diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs new file mode 100644 index 00000000..0f355a62 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs @@ -0,0 +1,93 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository +{ + public class Repository : IRepository + { + private CinemaContext _context; + public Repository(CinemaContext context) + { + _context = context; + } + + // Customer + public async Task AddCustomer(Customer customer) + { + await _context.Customers.AddAsync(customer); + await _context.SaveChangesAsync(); + return customer; + } + public async Task GetCustomerId(int id) + { + return await _context.Customers.FindAsync(id); + } + public async Task> GetCustomers() + { + return await _context.Customers.ToListAsync(); + } + public async Task UpdateCustomer(int id, Customer customer) + { + var target = await _context.Customers.FindAsync(id); + target = customer; + await _context.SaveChangesAsync(); + return target; + } + public async Task DeleteCustomer(int id) + { + var target = await _context.Customers.FindAsync(id); + if (target == null) return null; + _context.Customers.Remove(target); + await _context.SaveChangesAsync(); + return target; + } + + // Movie + public async Task AddMovie(Movie movie) + { + await _context.Movies.AddAsync(movie); + await _context.SaveChangesAsync(); + return movie; + } + + public async Task> GetMovies() + { + return await _context.Movies.ToListAsync(); + } + + public async Task GetMovieId(int id) + { + return await _context.Movies.FindAsync(id); + } + + public async Task UpdateMovie(int id, Movie movie) + { + var target = await _context.Movies.FindAsync(id); + target = movie; + await _context.SaveChangesAsync(); + return target; + } + + public async Task DeleteMovie(int id) + { + var target = await _context.Movies.FindAsync(id); + if (target == null) return null; + _context.Movies.Remove(target); + await _context.SaveChangesAsync(); + return target; + } + + public async Task AddScreening(Screening screening) + { + await _context.Screenings.AddAsync(screening); + await _context.SaveChangesAsync(); + return screening; + } + + public async Task> GetScreenings() + { + return await _context.Screenings.ToListAsync(); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs new file mode 100644 index 00000000..aa4411cc --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs @@ -0,0 +1,80 @@ +namespace workshop.webapi.Services; + +using api_cinema_challenge.Models; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +public class TokenService +{ + private const int ExpirationMinutes = 60; + private readonly ILogger _logger; + public TokenService(ILogger logger) + { + _logger = logger; + } + + public string CreateToken(ApplicationUser user) + { + + var expiration = DateTime.UtcNow.AddMinutes(ExpirationMinutes); + var token = CreateJwtToken( + CreateClaims(user), + CreateSigningCredentials(), + expiration + ); + var tokenHandler = new JwtSecurityTokenHandler(); + + _logger.LogInformation("JWT Token created"); + + return tokenHandler.WriteToken(token); + } + + private JwtSecurityToken CreateJwtToken(List claims, SigningCredentials credentials, + DateTime expiration) => + new( + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidIssuer"], + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidAudience"], + claims, + expires: expiration, + signingCredentials: credentials + ); + + private List CreateClaims(ApplicationUser user) + { + var jwtSub = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["JwtRegisteredClaimNamesSub"]; + + try + { + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, jwtSub), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()) + }; + + return claims; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + private SigningCredentials CreateSigningCredentials() + { + var symmetricSecurityKey = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["SymmetricSecurityKey"]; + + return new SigningCredentials( + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + SecurityAlgorithms.HmacSha256 + ); + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj index 11e5c66b..d7b0a739 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -8,15 +8,15 @@ - - - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -27,8 +27,4 @@ - - - - diff --git a/api-cinema-challenge/api-cinema-challenge/appsettings.example.json b/api-cinema-challenge/api-cinema-challenge/appsettings.example.json deleted file mode 100644 index b9175fe6..00000000 --- a/api-cinema-challenge/api-cinema-challenge/appsettings.example.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "ConnectionStrings": { - "DefaultConnectionString": "Host=HOST; Database=DATABASE; Username=USERNAME; Password=PASSWORD;" - } -} \ No newline at end of file From 323140ee6a476e8b2bf56de660e75da041a959d9 Mon Sep 17 00:00:00 2001 From: Isabell Date: Mon, 25 Aug 2025 14:07:49 +0200 Subject: [PATCH 2/3] Core done --- .../api-cinema-challenge/Endpoints/MovieEndpoints.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs index 2d05d666..4b7f2d9f 100644 --- a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs @@ -20,6 +20,7 @@ public static void ConfigureMovieEndpoint(this WebApplication app) } [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public static async Task AddMovie(IRepository repository, MoviePost movie) { Movie entity = new Movie(); @@ -35,12 +36,15 @@ public static async Task AddMovie(IRepository repository, MoviePost mov } [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public static async Task GetMovies(IRepository repository) { var results = await repository.GetMovies(); return TypedResults.Ok(results); } [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task UpdateMovie(IRepository repository, int id, MoviePut movie) { var entity = await repository.GetMovieId(id); @@ -57,6 +61,7 @@ public static async Task UpdateMovie(IRepository repository, int id, Mo } [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public static async Task DeleteMovie(IRepository repository, int id) { var results = await repository.DeleteMovie(id); @@ -66,6 +71,7 @@ public static async Task DeleteMovie(IRepository repository, int id) // Screenings [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public static async Task AddScreening(IRepository repository, ScreeningPost screening, int movieId) { Screening entity = new Screening(); @@ -80,6 +86,7 @@ public static async Task AddScreening(IRepository repository, Screening } [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public static async Task GetScreenings(IRepository repository, int movieId) { List screens = new List(); From 0094015c350ab803c1db4c5618163ef8ac943349 Mon Sep 17 00:00:00 2001 From: Isabelltran <128478218+Isabelltran@users.noreply.github.com> Date: Mon, 25 Aug 2025 14:25:14 +0200 Subject: [PATCH 3/3] Add ERD --- ERD_Cinema.drawio.png | Bin 0 -> 50332 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ERD_Cinema.drawio.png diff --git a/ERD_Cinema.drawio.png b/ERD_Cinema.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..8e7a497b42be535e4f5d9fc3e2fd29ddd758850d GIT binary patch literal 50332 zcmeFa2Ut{Dwl*wS3Md&viAshNlu+a#K|rFE0Toar6$O^$B1cIgNKygG!GMS&f})Z^ zNljKjK#~Lj$vH@nZ=VW4_w>Ef_xtAFf9AjQJk!&4_StdmwcZsE9dr(*O0$P~&yF2C zXw=n|FYMSsUI_k~rQ8i#B!5sC?$~i+(&4nOgO!UJ))KRWTSSTQi(6Rex{bX9w}>*g zu&}YMt$-=k*c@kUWiMcjaR5!=y|FpQn%F_h#tmy}Y0NFGA|@aNULDiHTASK9+k>B| zi{Sr|LZDeh5&Qx##6<}|UBpFsLCYyyTT6@%#zYef=2a0Bkq{6O2hGPc)h=qC;TBc` zzb&yA81OGu4BEm58lrR^XJZ9gRFEQ40$^(Vn%c%@#yIS@DWE+%VQ}_X8*9Qbge3)# z0!V1{BB!~9PJ!3Z|3s9CM*|Pm#$a0fk$OZ4RgTh3u7F#dMGuGZT&T z5^AP6aOB_5Mw-*gMHOdkdtJ-M6k`b{HgzFPffN-6hl6%z>Pl#ll#(DcnBz$65SN7- zw6Si)4I#m1p>gJpSW}EWaW+DegN==)1J?HUjc6NdYYdurKg1c0aX9Ew@CPI|nb}wp zPe*L91qc6K52!&KZEX3stvXm!hwH>sh!M93^;W}R&99SIMhX$%SQ(Q#6C3QW8v`C8 zjK?=^zZC)J||;KM_fV!NJCgILi4t)&Zk!YmA0S;S6L4bi3|gMcOx!R2ZBCh9pXa zCR@^mz$5zoh!h7~7Xxp=NMaxG%N**91;~#VwC!_le`RIkgvF?vlGX&r!!JwdLTcUC zM3~SX&;KY#D-#Tk)amaIRW^3OT*N~902~T%xBV%wH^Nc>?pVUIXk);4SO>rge{tBh zCBcrt`u~L`w~ckQH3ef$|K8v~o&w^jt?Ls85&>O_y#vn10`mt^{uAjow#1r43egf{ z1`Pum(g6$L{S>j;!3L5%dx#*`=4bGIlts5%E)by|YO(=i%`EW{ejRISiUG4K;%pp@ z9gGQ*4#~c)4InXID#R4I#S{TUDG3RPaf<=7LJ2$ri6uRQCa62kLCMA%oW&Tr8WCj- zfDva51SN|92!!}kS;6D*e=c@^3L8Y2AZhf!ixQ%u1UUI8LJ1MVj<%tM79_ZY)@?lY zZ}kHS9V9FNbLvNms8s$gU;PtmA0nr3<0}%j{x@qMfE;-9Nyq`~0L+cQ3h0}|aK;Y! z@qbYfY>opqIU0wxb%30Wzq%+v`QRL_9f0=I!de4&=`Sw3O^5tj|3EIpzqI~A;!*g2 zrveiD8wGR`i?$$z82+t+{zn}k5t4I5v~0vln&n?n1rg&c|MMz{=u%P+7$hD`|97epVhnYguqzSe@87P>{|AvYlHMTd5fRe! zzoH(I{2L2}WTyX5xia{y#9ws{Ha!xIl`$4X!~dd3u{q54Iur!{tHb{7i2VO45QfCV z=>JX)^0&#DztJ2RQ;-QGCSE{R2n38TU@Sqrz{dLY)*C{4hLE;Hg52QtBnHOX^c0kW z1NO=33vL*k%|#n6V`~7CgtW}oJ1b*r)3f+o49>>U+7tt&=%8pSSQY203q1%(Ns*rQ z@Xw+`BBTaoQYHr9T^R!5&B5m|I1p@b0O55)BjJ$H$v^{m_>;UBvB}=X5r7KtN?71GPx4=I_Rc0RK;0qV z|3ijQgtXp&ZGI6@)SyBj^j~2>e0NY#KuRk9=VOSy@kzqJ*YmIC5=H(jn)`<&)ISk6 zLJ4L(^65bs2a-+1$NvZxU9b)ir1FC@5+QtN&_Ea}DkcdH2G3h#p$CY2@rVi@{yPDc zC>(zuQ1P*j|NOGIAZ3$}ewWz(X8=@jpfK?XcOpE9k?@=ZM*ny~6(<2GaY;!eF>XX! z@}C<}#UOG)qDZ2c6!H1L21r}X@dv;b6Z!r8;$r{c`Tua^;^O~T^Z%;>>fc7a3AeZ{ z-u+JmsGGnDS)Y1%P!$!yBO}zX1&#P8h!IKH2*D~IjW-dMgs)qJeut;f4F8>gO7!Lb z*8r-BFww0c!NY$)p#Cw3AR+cI1*jr_1k``9g(~t#K>Y_>s3Ly^)PGS66_4ouLD?tC z9^NXj-ER8e-;zpt2``8PpbBTAm(hc0-m!yzhr05qi!Nm2MOGPazqk%NYjZyk^RC@_ z{%GJ{cHu`DQS!YBzHzgUKG9v=T|lWKWW{!0hINm>M%lBJN**dF1RKp9*&O|QPEDU! zNHZtv4`iK)J#fxg`qXJ5dRywCN-|3}$7Z$p4VeVTq}8rQue_zNHcPdQSK9P! z+VWekl&qc{y4h;rK6Nu~_9o+9Zx|OF8C4GZ^u+|ZCcZ-#g=40tyl=8%C(SNQJI&?g z@=>W74Zsr4k=s-0?+ZugXzY%nem;6^_uA^S{1y3KHaf{6WA_P@s40+lvI&(+?JwNv z@yuGCoHV&wFD2QoDTV_NxY&FaVe^rM-tz(8zV9W%Yi?;M5PE)+CC_|D2isTZ>7tQ& ze%t6$&_liAR+_(~Bsuka_ih^U8+DdsIN_ zVEJO?#pwcBDUQXw?}+k-kb4a^bg&6SF2RwmeY90@XAc~sBhHEo5TfYjCulU}<+mm##9d@{QcRvcepNH4?b{w@%Ma+R7G zY>~LO+H^XdJuI(jTDK;xss?Lw=jlVHbJMizR0Z;Qdf!!8qOx77;d|9chqBa^{j%qL z?S6`y3dtFMOt~<}&pE6MvXa;KwdaUWOIZ3La<}`2`e&55gv39{l$-xZ!e4^|xhKN= z+4?E*nNB3iGCeIfTJm!dyI9Ys%0&H4k;oT&1JTN}tWQdEQgfr}%cGxE=9=!e@ft`H zL{zoBWuQMd9oEZUa2nc;DpS^J@B#QK1a?b_hvk0sj=?xmwxhKbQg~im zPfHe7$sPbC)VJp7#j%epCJO zWrNqHV8v8FdfU-70I_s@KeNSi_MRavsb+*E-_uj!GQv-g#2>rKbl3sT@TBxwg=(mh zb_{4B2adKllm6E54e?x%EY1T?f4s4u9#17Cy=^$4U<{yO)!KY_!RFF%GO}GZ6YN)s zhytA$&H@;FIw@TS!lbMdWL26_<37-s!jcw`r#fLn*BK>qd#zMn3lJXRf4Sq5ngVk}dVU zJKB5r9FHMnUtF zQJrC*Zi-xVtxp(Dl54E~WlV2-FdjnEBNpaF6tCl)_Bvt^4lT4FTumnZ(}!GHZsaO} z$SNw{7S#H;bGcWC+{(DC1@HWRHxR>>lEZrLOg(IWau%yLXTWE3WgzEZ-)fb+ELtGf znd-zTg%1U=ledXD=tLFS_!*F@KS!2@a|TO2{AAiAMA$~H>{uFBFlt6L5?=L0F+-4B zaOQ$2Abl{{j)2CQ4FM)wt4%)ZJqGyf@!fV3K+BM|36fE@0A3jrJpo-lQ3j~P03B?@ zbJhb$3J9H$8+2H80Umo zvpXVZnz@AbfWw?WX(qu7<_z^_(lnCxscLq#8~1B}-n>BNqEp93Mg;_1f}SX7KHO9Z z@iVNIa(C`&55txkT3!X)^=quxM~)1eE{#T3OZBC0bf!0}hBPHT9sR!M!g;yFP%U=3 zdXW)PP*>&=8KfHarE_WblhVRECH;_v)URAV3-s+~7k5pGkI1~oa}2dlDdKfd0lKX` zEa=|1gaoFp&inH81Lb0Rju*W2;RB{i=G9{(_URttP6p$1*_tL?$|pWXwx4~{7osa- z7gu|-PBm{{JE;Y=(YL0c#j6dW7wKkF;%G@pWrp(<>MeVyo@jYeN zP=HrLz6FbBCGi58XS0S( z^nKLcsFRm*0QPB}bC4L|=(Xl%^RcE^XczycR>?cXC+A7VzHaiaIHl_(#4ifOTX1ib+k(!)%yOs8@EtA;Bs+R zccSXBqGa1w`*w#i7vfpS@rlBqP~ngJee_o}5`WX!*nj@+jyCS)`Q0dEq@474&G=4+ z1c)hf*a5IBsk0J*z0`?P^sI4sy>L=Ef5egFKA(Z63Y)xFV7!U@LIQx(?PRAG3%+P? z$tYlLXEwSfD~KX@OWi2pmjGopxrBgV(7PCbBQiTts{*pybY#8;kjO8HfqeWn?|EB5 zrs1bkU>#byebHc#_*p?)mNa=Mec_9%MfZsE+|6Ta@xb|}<%Rr{p&sclLxFq!l}&lE zBe&YCkwa!Q8Qhm5U;#khW$K1UGXX9d(A(bQL`=IZ-n0u5j8=OWRo<21^VSkWhT`+l z0@rHo``VsxgilnqH&%?rtJ^m2YY(@~H{@J9I1m}V63^S*m_GW2H^RO~W8=McS~xsa zR%<^Z+vdr+4yik33n+6{<`h zZM@&sJbdAlcT)5kL10B55d^k9$Dh7ps#@|Syg*PTF{WIGd*L{ma$qiE<8($6{YOKs zvYBji^f;%UBd5FxSl;kC57oRMFgREnt1kgizr?+hYW@~MisNo>mtrMLo=&Om)A~J1 z+yO3L+&^>Xo2*syp4+xrCrW+$%HLQc>Ek6p8=|V8rfZk)uD2)9t%ITYY?}3b1skC4 z+~=O@Kf|MuLuBbU(`#*&lc6*B<=g0TrSFcOA6dvLWQ~u!qx*8nj>Fxg$QYH!;&+@P zgFvG~ zuI>IhyIYWFC^9;FgI5quiJk0xP>@qdmRYZEW#8Zz#!0){>sH2}o~1KMZW#GmoxBJL z%tZZT+(Z18eL7E0so3*dMYm2So$c$I_%FHwI@I*>9*X1(e(-FUh{M_P5j(6FtW+XeqBXbY-0N7O57*ooK5^TYBPF`=&VWGY5?d6BD z%Mpx-qB_Hn=3aHLcj_q-;4q0ECj=T%NcAF!H_ zAERxT5M@`K)igh*I!x$fo&hNqpb?#R%jWz*XUheBthChUo$lvCl`Q63w${>y zf!REZHA!tnnP<%Jmofk_<2-Q!ujv5ase9WTFOX!KwYpL57Q4LrUBLleDR)fcxTlYC zepr0zY=O?SK;tJ$;^t6DdFp_0lkY836@?-TVGC*|})7vI8wsZKLHJ#itHMHySrUsWN(bR~XBbXX&l#QCdeztQX z86U-NJY!l75=~%;kEERvXGu<2NQg`D4@?cuo&T(I)Gmm1j9Xl4>?td|o8F$~M;ek4JFcZ#?_!VD|mMrE`2b<$=)|CfB~VgFR!xo~O1 zAF_Pzcy&D0jN27vhAjQVF2aLp=qH+U18?DWBV`od(9M_6E;t%S(Uyz-${0y#Ne;g! zY4tP_*F34Y^Vjn0-9xD6a(}p4@-joX$>v3B*}+GU(2aRM(QiCBx(x{VLZcAj~a@W zK5;xabL4#xwn;j8Afvcnd-jcnvBWSP87kcQ#TSn38n`{x8Lm>n)pFXy;EKqYY%PF1 zJg2gKUY*V>__-&tx9j0)NeD4v#myY#CI^tR^-&SVt(QaIzBUUHp1^dME9687-Z)+U z+PS}h`P;snz*VI;yW882lb5vVTD|G{VPJFVLxDlDN;IU}efpRby2QSz1sHwDtYDnO z*#>`Ej+{@G5hY`qR>$*>+N4IoS>WkLf6>G0^!wr^q_S;YnlBS) z!XgLDMocAq3G2F3PmeV_w`Tgr%F~ze5>ME1LAQhktp%JtL`=>2&@sVMvGFqrU9-G2 zCAx>TSZyK)?(CZTQ~}3nkss#<5Xg)2qlxe?fV?lE=p80^Ey&=4gVEYGO%3tw$2i3q zeuHJSji{P7q2#^hwGju9LF)*t!buJe?XKt=$u;;!>ih%v_Z)ONhgtp$?8JOF&sz3t z@eYl=<(9GVCk!`3+S4!BRV2VU;@?Ke;XIHXUw5KM=RT>h7SCfDbz=Y<8Jp4)eI(0} z9fAvU!t!YaaMpo}Di3xJiFbiTLBLT>18^zpX)ACQ51}fjQWC6jaH(K29*#N&|A5%Ni8pd^UYPa* zGyF={JG$qaQ9j5D1PZ&YmI$$*!n_TjXroOZ|Qj=eITQlWL!@k0o zLL)>&Ge$in!SX@GrO^ZrlSrvAo}vs8EIv-M6zrQbxv^}osF8S5>rzlP_6BcrxS&d` z%ffPUeQBCW$cJM$WgOSOsi8V=SEon2UU+#ujRs9U_c_c{P#zVG)Omd;L3*ME*{XQ5 z5}72OEmX4{Dc2#ihHc}@5(nzx?L)CFVD=s&7f>dTM|jhplxsF%7}z%-`CX@y{_Qa0 zrcCRZsi!_jXYS=!JQG(kq&f^uNQ5P)fx5d26)B3YI zCRU6bcac^e2SYc^d|w~v7isiN=gj}65%vjn`C1(7s|tg#K4)hf>^R~z@8zjsin*ky z1pm7?{XbHdCF|D72JN)Sl^m#!h;7v}%9z<-i1wVFB=BFWdT;7Zn4xJMNAZbU+Sg#m z2lXn|g}n5|Tuy`$;*wvS=LUS+4KfdDm|}eGTXji?8srqfv{30@0#Nc%I~Io5sQ_F z2V?xi>~(Uz?iAW8P^cL}A+eK+i<(B_WO&C=!8`g#Ui=+NM86<#5>*cw@Whl)kFb%SgS0V`#b2Wno0+T53_ToJUxw z2s+|Y`J-fEDaRoblm`|;J0}z%oE^LKxZE;;7lJ@DUnCe_LgM6;I^^8|^d=_X*kQJb z>2VN;xkjd}v^~Jj*e(k8{WM4>nP94?JU4|REq8%P`eh|cpG(c&Tjn1|NV( z<2B}oWCUZe8x{g-Y@eMN!OtHGPsmu@yDgU zmg*TzyR4D>-H9__%HRA_Xm=DjEaUU~<}Mp{V`8jQm*1z8adKXl0ZS>WER^;do)(!S z%bffM-B|vN^R84yIYq(Ai8)5O5aSbeCHKfM0;7Ylm9iXr<~U2l=c*%cIhjx zR1O`iERIop6Doaqt^Y!{T*saB=CA6D-iWsbvW`{Vo~g*x=$nLlws9l+nnRQdczk35 z^vKFWgh;Mb*P3 zfe2U_fFc%SV`AzlOMqNotha{j<6Wtqns8^6djDr-bjBLrB7V+TQa_B%a8x|Ou@*BN z6{5(-^TMECmHJ^|peF~WbsMoMYM??jmqUI#qxMn)KpKOZ_pHvT!?hdv?(8$f1v$EWv*rPi3_DC z>g89Fxn2=|g}Kn1Rh9Q5j5AN$?t0R2OStX0(P4=Q*i(?LK$)Y%3GxVJDZ!~YX@|b1 z+B-qJ3)Lfo#g!Lb@{(2y2b1^}!zJw4nVXAV``;amBX`D1Rh$;(u*|#NnE?CHuy8xy z&|cPRfWc2H-u~RMa{ks;uy%r``3Ezt!RN|u8EAB+}D)F*;L|92n872d?U7gR- ze}P}vA$ErjolAORbQdyH#DjgW3N?qRYZWG}OmPpPfX zv_EG`tEALutz*tz(!6TV(lP2aj&5>`^WtXlW$kM`ax=EKkm^-eeJai8nx5uMGeaUP zUuA3_J#JW`@1k_j?Z5pYQi}eQyt-15p91O&mk%ePxT>!1{n8pL_&JbUpdl+~5o~J< zjh-1Je>pi9b@w4_U!bj0uSw(~CBvYK+}tT{gZ|;nI$v$MO~!XQF2TO)U+KtVXhEG8)g`uTj80tCFPz)F=nB*u^{W+#86T|1ri zV0?5#XxUznU~Oi!3v5g9lb+BqXOr^cbrSes-m}s!f2242x7`BabBB?Yh)#7|qvBQP z8>(LP@lvD3s&4!=55Tdq!dpX0@!*q*Kf}Uj-sZwN#%0nui|_MC)wGQ6WC z8@^n%9sRzXD!JlNEkIU$L)WfgFS_ol@&_3+w#jP*#g)ZD{+_#GB%7m{-XPEjg+ZcN zvF@UB=2b~S=T+J4rGw}ysm$`tcs$@`y8T|9JpgeNvKYqz`xkjW>DH;@L(+nZlPo(jQA+2^|INp;!U|1 z8^(Vkwpvec%xt|}l`_p0aV<{PBTo)GsD&Bf)Au+-T%i5!dE0t(|E_gVN+PE^iFu5Kq;| zl#I%*H|;1F)j2)7cFg{ouelI5#EKJ2Xnjy}3dSEWYd?7cY8EFeI3{thl&ut%ce)>o{T+HmPfSLhv|)R{&GLGqs(-FMoo~7C(XPurRj>E%z{>oy+a5H z!=dTQ2O(U!t6)RL*5yK#7~O;i?1EE;>;SN@j!H>Nl;2o2t}re4Wxte`8qbDUVU+ z>rZ4d_ysqj2JztF+WnMk)4a(|rB`@fhA&fI?=e*kN#+s1scY+2cZGg-8JOcAl8TT+ z2<)k~kId{z&#}}1(9te6liBew{V3psbCCzW!SSbjFZ`JyP&g`QSUohN`Wwe@^to_M zZTk67h9ZXV|kad(tJ zT)wl)hz_P3E03{Bj)>|5m`uC$HD81P=?a3BxoT7yP^zcfKa?1Wg7c7}^rU1nlSgUo z(SIuOPZ)j@AhTQYaUuU;cHB9NYea{(Q}#`+?D zE0h3|?*|SRVxHs?Oc02>KqgiJs3*VM-x#R^k<*q9XwIt3zG3PjOvPw~u*t(AepaS?1i z{rWiAGDPb6P;V%m%mc0j^~eNNfDnm&x6}yioMP@+r;YMi(n?%HJYX6u0t!|e=H`lX0UYji-s|6LKtjy%OKyC5FT8nUhurp-i7LGg+EiD6bmwkf z0-OSzS~AIg_1Y+DS#Q{GU@ysJQH(m_=Qk3Ag>aBjQA0I2l8sG?cuXW*?HPd9i91p~ zBeqM~HBgu~vBHD{r3l`z6@^kUNEUtod&%ZDYnUd!OayAw z0HsyvVe#YUeLeuDr2WNEaEh=S(V{w^R$+!3L!B%`Qg`!yT|cQa9{WJ?7RGC-@e8v@OC z0dN$jpE6q|NOw!)s2o(LGhLGdwwbqZsGvZNwXhS!C#5w@ZxKwrw`h;m3z5@Q*UJ+z^?MomhNwWSct4jVDk>xe11W4 zym2Uh=k_b~2ZK31epr)=P@KpoZ|Z}#0k`)!q$N{~(QQQLuz zux}qnMxl2wHz)}@NAY8JliuDiGq7;O5ZcC_e9KSpK>^+6G@G4!48Ox4f@C@%is29n z3 zv<9t4shuBC$w#|59hJ6i+Yv~SG)*nz^|K0_!0x69=p8{ZC`_bxH&^d-u1}tz=cGKR z&aq7);>v;n)?=JL(y0ZJ zvghKF=s3F(J^oz`B+H0XmEBSOMsB_4k?c+rZrLPqL~$FpD!q2`Nem-LO!HNgou;AGp<*%G zdHJ}tab1RbtrEuIg!XS?f=K%A9M9aV2!lH^K~5e*mGlu|t8)Vt@CnBaQBZ40VhSOE zj|bf|bt;aM{dAgj%a@BPFnQAyI->jpC+wk76J4REGep5LzxKUcHX*^m!%LsR{aAnz z>hg@(`)?XP#cdJa4ukx3$>IB)DKp%auIU5#L`P9PYfj@;o31mAaDFlgLacQsumi8H z85id)-+ipYW+{j|?)ti@Tzwpy-hFhHZP9f6YkMlPDgNh-eu!N8oF^~Sz{~HHUrqNH zze=6PQR7Q0u+QPDGMFt@RmUb2BCuG~@4W^I#e6EZpj20X6iQrJaGw7tMe$_K3X}T{JvzS!3jDaFZ7{`&ajLs;tJn6gr z2K{m;-nC_2?H&CXRDcoAl#4u0W2m{ajqd&gw=&AWFN6SSycKg}C z8t@_V-#^)vT^s*hE*U55^jw6gIOMoVoNJ!*nZnqe6L{Aj0wG><+R0v;JL#e84nkQgusmLZAF~L=n^LgVc zRtlkVxc|VzGcuJ`v8gZkCg#suR|a)YT-&=- zrBWpI)(&l4#ZD%cgiW`+^I+nqf$;D)HGmWnNt%Jn4We&G&P`h%I)GQTK$QqoHGU>Y zqfb4(EvVTBAsga%4Wz-K7#e`(Vju5Y!^hig5gIV(Zu%Ga(Lylt$x}eof&E6=BXMDH z@18Ah>o<9ZP0i%V?cF;VOttLm`&o}A&OMXwP@Ne-tC|g>^T+kc5KQnZvnL84s{=VP zxDqsN3Xm;|b(ii0(m{a(@BZQVrqY6>a2_OK-ruIH=LSPNxqHX)8~Y>(cIhQ6sB?Ui z8u^MT5@X;%n^lf@o$KQVMd|(H&;X*hsAL%nluRvM=?ID9z_fPGgE9^i+0NT>nFfru z-tk&{ySv7yE9T?syY%afi-qN7X^BL^a#WaDbI#0iF~e{znrG{FlD);KGVYe1{NV06 zlz-gmQ_lFZU&EN>W(RP%arzj9OD)xe-HaEX%KGE)a181OtJeB>y9ja=)XmBb`sbD# zjr+N?9N@CGB@ZKUw*vS;rjDtwen(nr-*Wr@>=!0!<(VNxa2feUC(A3+g`FRk3$*3L z%t%;igeBh%$YI8ydvRLso)K$#Qg9KYMZ9G9=@Xm|hc--i%N>O(&o}Yg=q2pH{zAc{ zwql@k+p9F2_9_Ee*%sp<8Noa(k)Kx&ExSSWn9!Scak61xl&Yv0Y5PVK3b#G-dTiBa zgH5(b0#2L}RmumOx~kXrCuQ5$PULRu>vIZEoZ%lhMRz-=HJ6a`G-N`=$2nw8Av?|E zgr`LpE1S=m904L)99dYd`I)ehYh1nfPi*EB0pUBB*7LS|o%fh@@AV#)I{kjjdg215 z(tCOuoVR!vEvR|GT z7d9NzGkPbQnt{)UXFEJijsxfFn$(4zz^B@B_H-c!W#y2J-zLk)XqEQ7g|zbiS070H z2qt-_1>}7K(Yy9L(~MT3H^q0Uwa$@PKq^{cSFN?Vrcniq8sSqxdF0}BL)_qugTawi zwoN90?zi+Z#5cocj;xi-cd@w9gx;Rmcc40JmQbDbwFs)ta@nfRl9rP135;4ic!9_3 zb&)c4B|U|F2+pqeNvwE?RE>*AF+&<-vfIqd)r!Glgz6zfW5a18H9hb*qV$S+4&AJRO^8X`Pr zy!@$B*B~`>>GP|FyQL>StV(=se4Fi#KIxK4!FF-8v8p|q2{DnD;w ztl)^s1)+$_mQ~GIyO65fryt~7nk16C5{ZeG%Szs5Agp?aZLs8v@}Oyyj*nEKVkC!` z>)-)d@7CU^t7|>=bR9FQF1W1IML+q*`<5SSx(q(habVRbD(_X8c#$p==J#&pMG3Y? zlUsbil{;ppJ#@xM&LDB<(&$AkPlS->^lOPhxi6(-~Iydon@`If$x=@tNz-O?e9@Z}uvNJV*`LzB6 z9i7jIhRg5i=tW!#)Kx}a*?C@n?T4zG{jrL^!~L<+=%hM2fBt@{iaSgDXYL5u!OwPb zJqFQGS8jN+=A55PT~&u3T8%S;Pur--Mhr8My^)SB%M`sI65$zzsB&|8n@&a z?Np)b^5C%7r;{aa@+IHbUVpo&i=E3$+_0u%0>@*2cx5+Vav6*4Uc7t zM`~a6Qm0R^?XqA$umKNTj}_v-c`db_&u(gI-eorU*O}RG;WOcZH_!0C(e8BG?o($(4yh(6d*(H<%(_-UX2_rCty` zAA4739v6jhrlDBnmix<$ug*mo4XqW7thpmmPa%m{w3Bxu2!BN1~vSrtxWSw~%ex zmI2P#egY^FGglc9pxqX;i2}w-ChYO4E#D=d67CGCHw=}u!IpMUX$H1nqSr|Q)Hc0z zx73uJVt?U#p04$5^`P+i-m-^3aY5?q*KVNc#&3Zd%hsOMg*?L7e?Mr+z#CTqkYx1LW81hS8a&*Z0SQWbb)9t8-kN{LE0m4nw z^#+3j&+A@*&jFupfX@LRkB)8(Q5h6##!aOSz2XK+$+st#27J5VABq2Vfp-M@cHzdh zZx_CuuQ%M8x_(QPQ6AKC9I5QPjbx?$p2L*vWb*m)Q2+M>@*%=<@O2DJt17EpM+{}I z4$0nDVj3=+iK?_$24CB~POrq1M;Ed8Xet=UzO4Y`02F3d#tam@MM;k!cOWO$AKk#@OhGQjKgh- z`u&KU1mS_IE0b!ROE}=Db_qLgjazGmCj6ZkV4k2X@ zMCO81Pk8{{=2uGVn_ns6kq`fsQmQbmd`3AZ?aU8z1<%akW?NK+{!3|uzZWp{Wbw06-&K{L%j^8mmF@ z-&P3ViJzT)Bsc_y93|)z#?-6rv;6V#@z)KI8{%Rwo_y-2SIwKAC!Uh)T$SGvKc*vr z6-Ejb5>iEsF^auGIdi+Me2ulydlYqISKQ6pZ&$G=yUhI=e2->D~i4AC*~LM-=wmG zK6{LrZ|sA4rDfNGK2}het5gw|TUqKDcvbk$7)>-+$uYAlz{d%RC_S}HaEfd2Xllf> z89g75VCIc;h=o_rt+eMdZWL7#$|miYT#k3j?~9-3r2(eTpq2$F(DvAHc~u3`hYfy+b%SVr;!KK1n83!wK#9-V`@3u&%wZ+cEfbpZ|)<<2*r+#~1 z73oy$8L&LRpK~|5I4OY9uX*s|Qd7y9AULSnel8P~hpnoKVSLr2>T0gwm343pIaDEE z|MVVH@6to~F%8n^60F%`5F2%J_SP}NDe)%zS5DSooEGj^4HtmTUp0mNKX)R3acEsm zjAT7nxvV+A{mV?`Bq#R^`ua81#P%*7X}?O|#lu)Zr1hCRO7x_d=W)lLFE!jZO`cw1 zx7FL5?EYKf`!oh*yd`3$O$=Q#%*jYMfZK6@*hb0kgRIITR_%H}&$G^{je1Nw0~bON4Zk|XJ!CP0Nt|b2WT;G< z-CYx6x|PWKA&SS@*RvMrrAn8ecdi(=diS8_O9E|EVl8_j(G!#kpE-7S4b1=LMo?a<+B`umhMn6}Ws@2ll`OlMB??2rJqEym77)|_V}s@)m= zh7pk~QSS4CsjGfadS@GRlc*R&#A?OzKzaPv+^`RYEjlK19wQnWFrJV^2D3d0@riT42b+jyYqsPx7(n>7@HF4h$#Y!Ve=TG@3^+WBsTmR32J_^*thw|1#JX|*Z7g75YLT_`wu*`R+P=7bQN zQn~+0^pd8GjmhH>$rB&54ANL<)K380D_J_NQ2!7c_^8x}Za^jFsSD6~2jnwC5C|5# zL#5zj;6sl?4|;T23{%-w zgg1I8zdhH(`OzKiavIzxhguDjr>BE6PYYWULMV6vWzM9d8$3lLK*Kz-0do*t#l19Z(2%1z=h!&7L{1- zZdDOq!E0_jQVXW;RLM3^D&uo%;r-`-2~8x>f_k;(rm#LGPDE~9?OH18L~&}Q(~73d zfKW%Uyr*LsE9+2kN{_J~xhF{DP7G+<*8^CzBwy4pT7|g`h#2ZSoPO|86+L@Pvry+& z`1h0YRuMgKd<8v+Q~{ z;Inb&M?YJnI^FYgk##d>AUy78! zS)>;oL0dT^RAUB``wbM&$j5g?WcNouN)LzW&s70)m(_lnn8q-Qf{k*#wd!(3(?6wb zaVr$wZG7jqA=7AW0~Cg#)6uZz;2=eR%-EjrR1dd|gvF=15rNs-)3;#(ASosji5QXN zvNa;71(7&6#U1Bsi@H{DWBv05BfVkM!x8q5zS_O}mQpD$48$oTVGhl&0)OPjaS2g_ zvl^dNbtW+P0Gl_wi2drImQB~&L20GD{L)LiGR4+Tt!Q4o<4$$TL;XgG=H;0oiOlo1 zxHCm?L|MXGzvegoRP=P-;Ug!2XB1sKQ#gM9z50eE4_QBJOJU>T@2re7qcfVZi!YdB zepem-Q$E>k^^x5unjar|Rd`QnD-mt8KM|-dE1eqECzc)c%=1l~0<@#}zB9@XomevZ% zjL9m={s)qdC9j^$Xd+Fx!5_}K3G1Nj8OpTQRB}z{tk~ldKsog4bcnV_M_jT*pu*6r z3m!spDz|3dP_tGFJ;<%~Q0aT(I*MqIoH1sZl*_JkJM5T|(^m^E;@Q+Al|J}}dd6?R zT!>($=7qwm4~oZ}4$okPyC#H*W;Z^?pCS*V<#72~%fwJ~+7;BLe-&W8_t7vop#gWL zS~X5o-sW!OiqfaPD)ZHuLpLcuP}?NJ+lEB;{Cp9mjcwJIdz>Xj)gKvd1xnFpU!y=N zy4};n5*LQti2lCXq!>S0@D<)tg3Hge;wAs*^qFl>yjjPMb{B|FtbI`MFfi%oyuR#h zv(l>{u5|R~R?0`sh>VScku<(PX1f^0(p7uQLtpK|#L56wCjf9=l2aMJ6nK1NK zos5usOb;s22Wgos2DrI~loKR&+CRUt7N~dgC#7-2vffOf=MCTch!-xLWUkHh2obu<{XRn{>t)M54R17v@UyhB#_UAftopX_ zQKH*>Z%^Mk&#+2q)2ha*UmRoSA?ZSl>QVc9cLHF3r`F5n8y%rOInA+0Dz8XlZ1_2u zU>j$@q$^ZjXhEJ})~}P1K+QT>XsV=lHa5+RA^B565AL}}UZ~cc+n3WN53j+u<&_CL|KFITgcSKtwYajk6E5ErFWrhLnK+!*$7IK1vFs4_<6ulaaq zr8ZpoKh3>&IMwkVH_Yi|6j_z+mFn-EbY)RI%JFweFfGs-XQFE$)mQk2hfzsI$hd(cf$&UHf7 zuHzo!8NK%@%rcZ86nCB|*XjmWDA=VHi%+*tUPD5j@jvL5<1#;ms zq_o*PU%1C3|AjW>VM`#TG$GsurI+LjhzpDP$gvu(PNB6~U*>lssYhDI;vTsrxOExH0bw+lO&=yU8DqLO)NNa~UkPE;?*BV*Tck-3AIvxj4x(p4~ z!5ZE)X^1ERHl4JOH!G!rgJuqP{VMRM_|qff?yFA(*MmIHwT;fEJgV1XA{uJe{MJwF z1Gsm8btrSDf6OoZ_(CT=pTgtgP&PuYq;>hUK z&>eC=`*s2!OJh81XCHC%>U3Q_ZJiIEF^K%pYjL+7DP2<-c?|1IjpnKctXKU&eg{O7 z-WdW6lYl%7aH7$(px*><{9=_&j_0Qt5$@aYW%BLaxOZtM_`82$NxOq;keFCFD*O(d zG4Z#;1%r8s8UOl1O}BEHnYasVL=ACnSsaN()pxf~O%NKq?#}X4Bjpw($LgudS+7T= z3R%W-*Y-!i66vbxG|b}HebX&#-W0mMqe$%8>*Oq5#H_^7nJe#6;_EV>w2L?;U3nl-#0rk(CH#^1Dr~o)H3j z?46S-*qP-~Yirz!0`Nj0kx=zH&VsmSi;FTainlLcjdeKCJsW3;3lU-(4NM|0H8R^M zb(1}opz*T}Gs$CKlM|CCigdb%FnzkJme~@hUE8$y=1b!EptAH*6V-aM%0>pwE@TP1tV3NgxgHJ=Z&%@x3@UstiH|d{6OsdSg6>Ys?c>`3{TAZp3%;f z7@PtOAkPb>BI;ruv_0QTk1vM4^h9qTiyv2J82LGh68awjIM(IZc0+>~j2IZALmjiD zL~RU&uMMCa_O)iQg4b4NoW7`BD*ml{dJGWUp2v!Mz0i-wfD1VU%VrcvDEZXCkeXwm zDz~eBiC(t9Vr)6OyV3d@9cRt;rH*R41}-L~zZqPVHL@^KY4n|Aq;SeeICFU%Oz8FRGuz3u2|qv@>x zL9r&PQaBbBhN5>OhL2LuOHInV0sP(q83C+%jK36dj!we97lDiiw%)Zwz8((r9@lvb z$A@-Yw!FZGe?n~Kju%$DZ&yVS)hsFs*KhBMDHfeN>ABnIcX2B~RjQUDC>aL=8uq!B zM#^LQj*(|HI{ZspC}PG)r%VO$VlS7=s)ps>Rb+g0+;pV-h*qew%1ET(dhXuHTB;p+ zvg|q4Gow#1=i6)VFBo31H{XKKHd3xD@ZG!G{N~g~@=UVGc4+QNo7Y>bWFBZg>y)o= z-nS=k8%E{si3pCTi9Rc!^{tR}zL)IJ?@P@Z9sCe5LUwMKUV;mGz8&_le%2E-kTnV_ z-YM7UtTr+-`89a z{9Wb0Ho0s|7U&r7!Ea>1nHf!w$jb5tQ0q{K-KS|EE%FzBTogEuDtnW4@0xp%5#)b> zSxX}pA95nvzMx(6`c6eSV(;8)UAHIAR*vBw23n*qQ`sf6oGr2qgub`WfZ1zCwQb*+A>y&p!YWN^;t(KpqjdCDcj>av_bb;~uWxK$?(~k9d3!dkTyC`mal9hd zW3Z=wC*!2Xc+RlN6wkNFjcBpiEy|tgKveSj@3YmSrX$WB_m;Zm%DpxJ`nBj-qWKJx z{+}Gu#c=c506ELY$)sC8T6*p)1^i{pZJt|8a(hdj%19YTPx8L&lK$9_{3O;j$|uKC zPs~!6X*hQCvf%(T)f1a6OB63U`sgmxdt;>g&s<&?j8fkD(Q$OKt4y6_Fw{oD;}0uSTzI(n&v{2@1&{!1h#aPSJ+nAsXm9ukxu%Zf8<$udHIH zuRZ|$IZFe>Af*6F6lP`!Ar{=`SMGwR$tJr4V_MMTQkQzlx z)9~-`b30MYOmjS3O}%F=zQfx^QNO!NntkWoN3zNW#;Dl7a#qgX>E|*Odx4W<4|41F zSA^VZlwWX;mngn-I@e0jU`2BSN)0$% zlqB@Y=r8*=n8e3l5tJ1MV~?C(?+KY3$rvTrEK2e|?$MZ>$b>0ECac z^x$;YGS3PQYx91+!}bW+=H}X$3pg(s62zic^|Ud$obOdg5Ocl? zM>;vvCb(t+L?kSv{U*}yjbK!UZIjm&$+l7RA>tlnsWK*xYS*v{nJ7|3 z@7$}F9SOH?)Tr&S-IS=zF1VHOMRufunv>>Srv4qI8~?`-AF{kQy0e#*p{cTOg&0=LNJdQvdv99`h18 zyS0^u>$eQjEQRMYdpry#SNn;JroV#kqCR|Bm*2WW|LKq58~fdgC{dsZezO4An%Y`3 z`*=j0SmMv;S_2b259Q@9pS|<~U2|UeMLDIPwe^s4X=RXx=3?9RzozXvL&b=))Q%Cu zm=M`mn{G=$CBWMXlbl%YjS=C0qjm^fS}%_7*ydcAOdr{Oq%#E@2QW5%;UUfzp3^#F!wU- z0&4TCR?1S!!07vX;Jx4wdyu2R^j0MoPda8rW5E3sP8?de3bcGQLA?Kk9zgd{C>BzG&lx+TauS+t>ku5%L_SiU^YmNe_ToEDWno*cflU z%BlT`Ve6q@lXI_tw%vnGvS@S&mT`W!KX!j4Nu>CLPC9F*om1}Z^`#pzVwOv?Btc^5 zJvp2Gzk=YvU}B}KsDDHH(zd`4pwD;r@sQEg=}ft_gULcuWH2`oVif?Ah<{mo3b-4- z@cz$mO{F9^x|$Ih z?MNrL59H~|N0o)#=sZh$vzwa`1tL|6LA4S8 zLdLc2g-YG%z*96AV}4{S>2rew!fg?U0|z&X743S7+}7<1$G6$aE2GKM5|LweXIUCC zPP#D;@ZpU*+A6JV*7^C-z?V!nq*#UTO;wAJ2$H=EyGTA4b_HuY2r3tpkw}Ti+R)IN zE8p9+g6!GEbCiya*3q_bQnCErAVSIt`dhmNac))S$f~{2bd$he+J4NvV(s?Gq=(0F zg9ZsV@8`DZ#9WIR3@4C%>$8G>Uw2_3?I$ITrpWQRPYLNI`)LXbO6&)GtyA^aR2C%` z=2Y%-vy#cd*#_L|G&bB^|Cp1$4HkIBB&*F-`MrBF_$|@3YNL%H$H7lcaYTmOX|$KA zz5Axq8LjK%mLr|d8~Wd;xtRbkd~?6nnZMCqi|03fH>2Y5gc9JupE=$#*aYDvSNH`m zN5aI++!Wq50U9@TzhcPW8ApT=OXykoBJ^~tspXrf-Zhb{R`yKu+hj^lf}J4T+)=17~|yfP=W3AS0?)<-d7k@>$W! z>**M(T)*pAL~MR~p|+hj62iOe5RpzBiS4S}FV8%5idcG03JGAP^n$ogto;kaeTqe~ z-sq2NO+Vp(rfKD}d2d=8HrCNZyYU_?Ecdn^OK$cS8ljJ}p1N%#$Oy`~X4a3{IPbd! z+ORmA@`RtZyevKC)pcV4=UC0L=3KNq#cZaE$_ENhwPt{yLz|8v5P9~tLi`+Dns{TR2x%QG{6>hkO7KsZYV zU~pVDL1rwh%Q;3h)yCCn!IO7vGna zzo9EwOrOO;IjFcrtL-`vmn?qb)i`k(xGyfJJ379Dve*%U7r8nVU?X7){Lbq2P7cvV zAR-#1BS4~>)nS3lv^pbSIK=sPRp|B`>*)Lh5v|Y(LXRfve2Q=xfv7LFU}KQD=cGkC z3yed+_(>4cZSUzmQVc7A2i$gi>FS3Tl@AUaN{~VO3DrcrNWOI-C8t>QZv&xX0xnIZ zpgW9M0o|I^03P_jLvp4o?P$6CX>cKzz8P2m@GMzMD-a>~f`hjDU~!a@z4J9f{%lDH{(BoigXV6tU!GvL?-zXGktD93EKo;cNx&0XtcK2)bqDr z$h4Z8%0cDngu^%0^vkg`drSk~`(jmp0KQ(|r=<71WY$^hOvT)k-WkGC66owDk#(Ab z@6F`Yze3!|d=q&!G_XZO@DG9mbKdsD){{Rt;J6xKO}GqFAu-LAp`tu;G$Tx^eoxrNIAjf8ixKU-PFuo9>R3A)@KY`2p0Mrf^aghCUGO2R6ZeYLahCFL zGVk`!9ixU?)!8rODNAr@@9w@_mtyJ22EZt^)*C^Q;TK*(0r()dChPbEBA4m8aD1(` zG=~263ia#4vtbdIAz{DYh>*F{q<4^|SZPmJ1eQ^=IRLt$FCtyYL{UCiS>MNJ1&*PO z$M@sbWJJ0UmK5+tg(6~b=NXpw$8oe5m(mAf(ns1v*EvN4OPxfnwt zi(ZW^-4RT->Ko$%SH0TJd+qm1J_cQtp3`h%Z$*G&T5u9Ijl(2hm$V;g&#+_?Ykj`cjAQ*LlRXs@*CEE#~E-@)(c2Eq;C0Ymd ze!r>l;d3?jg!rAG0BTBF5l_PPwOboFu5wd$v=X=0@mLM!n{&ufM#0^RfT>eWa^D&z zdCG~}3N>}inK6mhI%-L>=1iMuJq;y{aYA{uHcw0Y%sN3)RUfApc3_uV=Ws9d!^Jbk zEO@^~ialrH%jn9max8HE7D#5;95k9!Ee_5;%D={auuZ^(X?7)Q_i@Pe-Ai|%nig&TydMrPk~Se@d7z5O^V?KrWb{pFARHTD8H;24#y^Pj>wSj|D*$ z<&A1XWzcq|p~iOvh3P@i@k}8bG{Oj zXNUe(0ApdS54S_$rtFg{%#D;^Z%?}*{WEiUvsh1~orQ~@e>}sRD1U`gmu>a5oj)Y( zAjvYABI=rB`C=zmt(-i0u>48f=G_K1=1(bDMtqingA$)7*RYd?j0ma6U4z$m<7rrV zd(<=Be!z2Elv5D3-!qnupC(I3hUmTYQAJJ92Ju%#uMmFAeiH)-&62P?(LQpxN``Cb zAC%&;Xmb-u8O!N|C|^|YrN7p-TE8ZW~@h$ zyC4K+H@FHVV16`137GgY(Y5B@yKEj=2t+M)3HS37!{rC4IA1m4Q!}yfRbb_=-Mo7s ze6u3(^e}-$GCa2^-jSWF;II8hL-E2&T22C@p=dpzp?G#kLlN+ohT>U7xcWW~1<{X3 zTa@Og)Z=oUuIBYdGIMDvTMz2eYpPH(f`q16GH>mXJ&&yNwc9+tQy00ggi_I!e5z-@ zMXi8JWtrTQ2FsOMOnxrKDt)Z2HO^;BAGmR6?^6T4Q1r?q(26H%OJHAuW3^F<~xPJ*JAYgUHrIP~ z|M)`^`c9w!OvgYLS+p`xYSdw;_7$S{F^Ye*K&q$zKcodJNH~WogJ?;E^&Kd2M~H?6 zA1S5RQeG4V((xlHuiG2kgEIgP(VBCEjJ7Y6<;Z1`G*=^HmWGSRXWMLE20ppcZ9`Td zLHsO!ip;)Z+@-gcjlfoPBB?om&y$(iEFw6+eXip&XwATp+v9nxTT070NJE-$~wPRel#CIJkw|Ky&sx(V}_(ap}G=O67lejm84G5#LDY~M(|18+Sh zapyQYI#P~u3P>!;{*@Xg4v4zCDC=oPehxiS{Oh$2Hcs+*R3{~VRZk1~p}7r$da-xu zXLN3@Kc>4!Wm?rOrWhkLtl^e{T5!B`M{!oI@zS(byUPEV7zi?lKtTZ`R)KLVs{e0U zE9HBDknA+Mi?gM05OpaaX&pei#_>}0UxN+&0ca@4=$0=#*+;nZj{zH;as8Ns>%I!X z5i_E#z`cH3W&?j=QXu66WW%wy?rr}B^YtW4Ai>>T4YDE3qC8+_CV+O148+y=qsk^@ z|AF@aO}>#Eqs72_Z{KK}EPp(`AGJYVIVDQGIupZh)SJ`nWV5Y4t@Es5Vlz8Ss1HOnq8Ftj7}g-y=CiNSt|tt^T_Sw9}I0=4#odS9r@L?7mz-tOQw9sNMT z*PYX-TI+La8zF6z)^EzK(g;r)P(Hj>l@UIQ8u{JMQ37B8@UUivFG$cBP%Fr}uYvLi zGk>r4pa`^i&)NYi5Y|ShVUo=}v~~gq6e4UdCY>C3JU47iZO;}Vmb?4drMoJ&qAg4s+o{ylo8^1(u_?^aJ zGR;nTsh!4aTX#f6g1(l39Msh7aD8p#CdnsVihu$`>Y3m4*?kk12R_)G_5Ujxpx|^~ z)%X7xHN0<891sCLsQ5om1SA5HU*yUp=M0J zEPU>5tXkgp08!V6YSnuN41B>ciYj1TM*QYF_t)hkypS8cAD5hKG6MBqH{h=xh74bu!; z&}S*n-@Tf1G@A3lH4YG$v~Xy=k60LLz&i*_PMIc=DKNLU^k$f{K{97p!LgwV^hW?5 zS2MZ%kx-Ihc$o?{zV0h zkD|4-E`&x-)ok|@b~*YB^RES^CFC<1SHniNwrA_lFJ6|u^IuJWvfkj0Wa*qdFJUR0 zc`^rh(w~_(DLxYk!TA(p1eYVlg95j7M@S#XeSVEn#5vN+z>#)D#=YbR@F5ep-4FOP1k7M=!!SL&q&tx$ zk6O1G0bI{`4*{Olb8Yk+^sMDbw0>6;HTC>e!Xtpa2Rn(+3NqDd?T;v)EMIgdeC~#( z8fmWy+EJjJ9 z-j&d)Q-=ouQNzf!sHU&2a6wW z%`_f%AG)r89843B*ApTf-+|7HLwyaV1b%+Mt{>;4$x>O(YyoRyZRyit^)dKe04jy| zmbqELb_7}+1UghWGPLwOH~`tP3j}-3_(0~%Hk*Ubzwhh^Zyxp7oh?7IK!8o?xTP8* zuHnE&GV}AwyK!U_{JR?;y!p8Ps!7BLb-d4JO;vKL>AzRh;3-(f5OXMBT~NGc@xJu48N*74t; zI%vJ*$-t3!oqwhC4thWU91vSW|Iq{={Ex93TJP37u%*saaGJj##poVkhWg8qrJn_E z{zu6So@Px+Gn{?qD9a4w*#Bj~{d=c8%m;F%8;Q315*bTn7+MeG-IKB*e((a%`yysjeGtj2cQ8+IC>X8GX42Gc%8}hh+NEZfuM-=#|2QLx;hbFjP-xeMRxteHv@NzPu z=7)dx2>{bD3y4(qk^RX&(vL#l5ImAzVde^So#^3FO9Cr>mU8^ay;W?l7`dE|Rqg|; zaeold0UK{vR@)IAgTI_jBoG1NUk+N_V%HFnv}+%>usN8AqsIQfsrXF=rZF)g%Jh)^ zEJ+!zqpSvPp~WPfv?J4434SB1ZMyXzc<8T?&bhOV+^-_0q#?t1h=SQSyGN1w^!(=V8XN-+Mn!yGzb<8yaWp^1imryt;nl?@0O_k;{bdj97V!=g8Wz0I;d6uFj{WIl0CB#n2Awo* zSUczyMzncEi(OBu0;6K|5a)wZn){9Lz&6|gD&ij*C2rSfz+I$5pA{5@Ha|b2U;i=_(^=cN&pJ*xTRylbDkrb<1Ww1w=y`Alw#AvCb^~fMGQ7LbLKNL{r_n z7uYd}>-}aDG#dvdU5nUC$o1aYwd+B?H)S}VSNMnRCU48*u{|z2 zS1I!#`^xD(&h|aC=?DNmGG*6)0?sECvbg$W5`+Xv;?WB|*vVXz$*#5qV?m@x_)IEB zedI>f7HDu6e`bF^6j|{ZFI$ZRtW3T1cik&uh|L9e0o8DRl zn|mlmoY#ZDD6x&42&Z8CfW^(b+yquFmmKT6+F=$Y-GeNam20>NM>;#R-7iY;a^f|Q z7`<=(x#3NZ|EpcQ} zD}`QN+HnM}Y$Nv|OQ;pl0vGUd?XZ7xhBY5TE72z)J%IOv{P4@N!o6}iJ~4n3W3$b0 z(7F=RL;xdvVlYIG8P)Z7WRR4n1Q)V%e?2(so62a2OywwB3v{aG5BtQKRnY%a80hQZ z)z=o22KMEt$~k>tjtjU!vRuv(SJn$DDxe;Lq1E?iVd{v)2lP-U2Fqn7x?~>1){K`5 zY9FWCwr^aX{&iPal|FQi-nv{G^eA`)1A_R7PGpkto>IVOcVBA%JW3{+GkTy5wOKx$ zsw{~N7SDhyUP+2;WkSiwok`xkWfLUC`6V>X1GF8Z!2wxpF6$TwtX9g99Y{=Xd?-r( z`(be;h&%I2{V$tVYAT}TaZ}uS91I=D{pdXg(nrJpE7}uHW&MU9K+$^O0Z8K+a;Hfz zLGhn5J6?v22L|7705!Cf*oO6`Sd37@-w_Hn!;1o z%9;%;FO*;Foj#Con#AH8A0TC;+TjGGvUkDfAcb6|#I{%CWw!_OL08{zYoTi{Q2HDi zIROMUc||xWs(4j@!qzU&5cz~3x%>Z+0OkMI!w#e-Tv2F9aTod9LHlcepHTh=PK;$@ zVuGpV5$ZQZD3sfKZLW6TvIv6r=fP_s`n1~g*#z8g&=Ar7(0*F})Mt6;pFRZ5R=|Fe zR(6E&P#~3oqizGl8^9593bHuDM5<2*cCv`jOy5!S^B?IX`ZR1UeW$ZK%MR*D70;k; z{4_7hzERH9b9y6K#l^WX{}v1htqN$Ad)P%bkEo-9&M7Jp$72b7pf9@ixaBz1$g_ds zeEJ6rN8@2m^xMXN=R}dUQ8ob&kVxvS*D?#R<|A1v(q;V38xsN4kqS=t|uXTt0yeo=p+V{?f>A66o-Qm z&?6hU1%@lWPNf$}ap}<_)>mc*m-{_&rZx0z7Dd~;#}rZ*C_nVcNP@T9um*zY6>5tBVi*c#i;jel52G8 zPqH)bg*H?#-@P9*vGa?%hPk3BjqH#;B;5ZZ3FKk<5tzTKRyv?*@jL|_~49J^vIF?{vcB7w-@CqCj{V7{$jdj(7k1S7(1t|1tLwPYYIFv8J&teG zs*PBvK{2!(@OS}5Ab+J#4IYdcM=p2_=xN5^KK9y&Y7fW;1kke{jg^W2+yEe# zTmZpYUnXYE0C$=I`=L$%(1p}s0n4Siy8eZVAc*ye!DA0i<*xq|KFr=Tz@D~1RpcpS znc=f7%;oiBJsluT3d~G~DVL{tiT7N+jtidKXvK6fBE&M2*Kg=2nS6HmRh=fVvd&)b4nS2G0&lO-an*t$B7^r$#p^}X{x0`+l! zjMZTe+jLMKBrH;Oh8wh9Re;fDT-|fj`$kD7 z6fR&?W>W&po(bQpAvKC;WM_LA3n-)1W|v_y&OXhaNsi(;%eoOKB8y;SF^D-<8MKep z@XIrZW7ms|bNT$IZd<;aH9;e5u#v12lu)Ki3jXjTD>sru)rLdlJhRH~>IQt{)hHVO zEvOkIq-^Bnh_W6<0+&o*#2^0k1~&Eh<7;q;+P-%FGblHLU>T1tHc@HVhDeRpI87Hv zP?Q4UX}F=Z1sTvA+~ zEFN`zG+G4vNQ)J3Q$m2wF(o%QXkLtAe-^rq;QF#6CG>5dM3*E19GCgfZqgh22z;ci z^C*qu17*U?sBpb%)~+++Gc}>BHevWebs(sM>*oWgf-KiFQ8y<;+H_3y;}EGfaW2JJ>Kpw7W2DyA zkJNPDM4$HBv3{y5x3%sOHyF6jPN827qzs$KxtM>q}VbxVMX*dL1AMNRs|%NCE~4`_1`EYDDLQRRuJLD>Z?6?xlnyH`~=b7Q+#*jTb9G&sFl`xboc% z3(jxm1>&1QrWA2>hH?Eil)<%Lm zR+14rzvo_QwA|mll$BE4x-%xSdp~G7eY@3xhTw*wD{y6gNLCn@k!f+5s z+f?@2Yh86*$<~3rVVO@fiWNR(khs~AzB^;vvQP=5EyDpRSl2vnG?#Vkv266@3aP#P zDYM;rAayD4Ld41v9w8RwG*4FIr>Eps+Y@(}rz$qyRBQ}Z>~`%*vV4I0x|yse8txfQ z%Z0d0u6ow2-_6+*lL(Ng!x29~axV(v`Gh4XaI`){%Xn_4dv2~QwaD$IW0+j0TfS!) z>+CE?NtJ*d)%3J~j2Co4c+)fT6vUQYuA?EcMn)*b+FE7B<-=jM=7;cDpW9v0+gsre zGe$XrhUCTbsfl9wVWx*_VZU8_X{sNcz8Sn%_m=$nx5u#pX=>r`z${q03 z8L+^8)@{nLK4x^fxS65uLJg|*mvSRWB@^73wtvnsv|4}xYOA4;}#YCtjq7!ax6m|<`G$o({=DK$&3OST_Rf2rqo1Yl+TyLn@ zZrEF1z##TgLuEJLv!*i{U$tb8W(V!XWGG7wT}42XF%9B*UPDLKnE?wz15^uD`MHMk zLoZhq>xTk(d0eQzv3tA2JtBJ9hg}~d?K~&+wgWvEGW51C4xYS!=U0bG3k`7L1)!Ky zn2c3{6cKDJp9;TjlFJ^cE{Ibv@d;2#jZBa3w~4-tNR7J&_-|5EL*F0tRb`2>|LSPEUEKA^oqhT}|7M=>3LN2NR*Wp_MN4gSpu4Ptl9_Jt`BOtL1-qu|=7K4FoGq`@dh7Vk zW@b>cY=TPt+V3V(aY9R!oHZ|9>L}XcNb>yJ+rJl!H+e9R^%<-$w%JQuI#(evleR#C zT}uHCgm-)RV2gXqoJDe*yK5B{eaqI0@v9y6g;$pXg*$o&E04uRE6 zB)JWph3ty`=a|Wb)a`W z^;{9F*9|88=@tHly@083V2hGs;_Wdz7PY(8-n{bC z5YV2;0{n?Y6R%|o&u-kJ&7$_7@lj^<&*vtW9U0*LfcCo;O6FbUX5?t)f%z+68Bb9& ziT+A)g^d+f*4tnD2n<`Z-_?Db4IflyThkb(S2v6dg9%;Oy>*+F>qndcRaG)Sj8 z6{M6YmVG^|LrJGHiELr}r%1$tjybl;>@P3hIbd8O#|<*!#VTH6Cf3)^dcV~F6|9|w zLT$Dv;onk<$%N62iOi)6CA@e{Qft#&Z|^XlHq(Wq7OOS>R@Z<$y@XETfD&bw!kmez(upsg~Wxf zAQ+Sfu6#2#V|FM#f%>BdA>ILj`PKHxBepW=#eHJ%&P|d1)K*rg7p}hrXzE;9!Y=Qp ztbY>p8jN$>u^33W(Py3OJlQW^N-iZK%1hw~dW5E`Z2hFY4r&3*l+0Ikhe);z6W6-v zHFQOgFZSIM{WtIztWE`WtwkK4NMUNmZKH;m>0bJ}gLZ@NuYOQt{M~gIp~AvLs7JS5 z-E1>bS$2xd*KVK9{IK>qTNOl&`_Fc@8(vLeX(g{5dlUue8<>JydUA~oIw-!!8Jw+X zXqV+5-iHO!-W-3Yi%gPJkxGAC=RoOh#N_5c$GM%9Gl}V@9tDUTF3tQ~m>cvnS)pGI zI(kfP=dyw?j$k$y9=hdbsuxNr4%vfXsK{vRAdBt3o!<%qn&y;%O3Q=NNvc87HzAUN zWmA7~%K&(dl7Zy1xCZr@rEuYKD;qeYhgQn=O%j&qEPDfng=oUC?{xla*ID}+*jTQE zBD>pk6@Bggo!e>mK`KW|vpm8R3V1Pt?`NVogaZaq}Cp9prz3psY@Fn?UtzFaQU z4TpJr@}Zq>yUk*DZ_YNOpQ?me5z+X^`aLMY#zj=(*=}mTdR9PC^$tXc4r>Zi|IgJF z;szlD=4fVE!M2VvS{{$$d%&Z~k^dLH27l&AekyRk^z@9cpxIrrZ`6ucr`byHvAJ7q z7}P$XI|zgS(>v$L|5X$QlWCDU6){m@>P(@U5h|<*6$G+dKR>}iU++>c-6b|+?b49( zG}-y7y}GooYgz^BN;aXeLATg?8?h`s~ht|)J6c_LDz8S#c3YoGqO~#dDVS#K<|Uj zM@5>%3@qrM$8?`ga5x4{k?36)GazSapzm~&NR&l-HVpK$_5+Yxf3#pw4H<>gwLhVl zb7>rO5*HP39#*TcnEY=R6hgM69jIY0Xi&p^QPLOsuRepm|L6J)f(}9yRwXh}#;NC+ zZP5_{7YLLrU#+s`zsw_$dht+MLbah4#r}of!<1IW5E=FHxvz=1CM<^!$chA3@kb}p z!T$)V%K$k#HT)ESLU3`-N+NO4?Deh$xV=7%?nqq%$Vviyhzs3J*SjP55P4-FU89oh zaDfqTK1wTKE&<#JP(aPbDSq%`MQrB<|D39i^m?wYI=vIAwxjcx%{sM4DE~@VH6UH% zmFHs$WLL=uoi2R#ZWwLULI}&&#KQ^4OFYvZei7!zB+t60bnUyFW01YoFgs}mtTQV_y2BD zp=nSoEgl}hl+u+;xBtH=FdX_Sfh#>Yaf!>M6+G%5=U*DBVrZDk{XBXh7Nw9Bwb#MHgoL+n^I|Gk>+4lYCgwY|i6*COi_|6}52 z%~trc)$#;ZSbw;L)u{Ez4LrC!J2^h35;Kbd9;Mf2@cT$TDbpN`L*MSrwc--p2P
Jb`U?Y({Yw0dzW`(WPH-;g_u`h4j zcX+Vwo*x@mjd*&4Qw4wSQYWL0NA1A7u=vNLq#ohV*-R+55HYZ|YI^H-QM-M$*=m2^ z&$FvrMV<1hI#`X0WP2k{5t&Zd4&z)(N zd5xxp-;bZRch6ZVcw>3oX6S@v&Tt<_a;@hq`U_p}pLqxR^!|dTq#2T-P{l!d<4XNL zyk0RW3&)VJa`^@i=GTARpY*W!TOSgdp6n37O(l1 z#8Qsx?b*z*qH@+gZu?3r_Z^pCUtUXMwq^<~OMh;e>rkhhEiL%g$A9dn$<`%T>ps}6 zqWZ>)wwrpZV2me|wq8Fl-xltjr4G~#ol^F8Gg4e^4|6xnY#w4u#fnYA7Obb3<-O26 z_|(*=$>(*+`;v)DU&&YY^q&=w+gj{#!G6&l_gm;+Jo)J4_Y+o9CD};ly!b!CBRL`E z`kmaSiNd)KA2bHOax_YQSz9i~GV!A+9S=3WHW&9{UI^eVs2I_#X`R@$*-g z(((_k-WV4+K1`Vcl=Y{B6;b=4fr4(worwi-CTi5ePBHN%u^;}XC66vKWqppjrvIzg zAQkf`7G~@K)=I(a;65u$aGUF!A61y-!@IC7aCj-1U=x87bWslVD+Mof*-0X&IrkUA zp$E6kgFy`*+atUgbnt^bR`fW$v_QhrMHOrpQ(`ARFa`^xJlfS(BdrQILg;B2qBu^h zOd4T5C0L{VTg8*Tyw-vWW6vG_P*@Ir*tAU|Jh;o-mAdfHI=obCY|U!_TfrM_!GKzL zv)p0uQ`9$Kg(7Jr|fQ) zmzQI%MzSFoN+GBm0&|I`JidDj4{vWT;1*vp9?*M`+B=5lt3gt;LzCPMe!)|^ioBA2 IS^wGp1(>H@^#A|> literal 0 HcmV?d00001