diff --git a/.gitignore b/.gitignore index cf332414..bfcb6924 100644 --- a/.gitignore +++ b/.gitignore @@ -368,3 +368,4 @@ FodyWeavers.xsd */**/bin/Release */**/obj/Debug */**/obj/Release +/api-cinema-challenge/api-cinema-challenge/Migrations diff --git a/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs new file mode 100644 index 00000000..837a07fa --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs @@ -0,0 +1,99 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.DTOs.Auth; +using api_cinema_challenge.Enums; +using api_cinema_challenge.Models; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Data; + +namespace api_cinema_challenge.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class UsersController : ControllerBase + { + private readonly UserManager _userManager; + private readonly CinemaContext _context; + private readonly TokenService _tokenService; + + public UsersController(UserManager userManager, CinemaContext context, + TokenService tokenService, ILogger logger) + { + _userManager = userManager; + _context = context; + _tokenService = tokenService; + } + + + [HttpPost] + [Route("register")] + public async Task Register(RegistrationRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var result = await _userManager.CreateAsync( + new ApplicationUser { UserName = request.Username, Email = request.Email, Role = request.Role }, + request.Password! + ); + + if (result.Succeeded) + { + request.Password = ""; + return CreatedAtAction(nameof(Register), new { email = request.Email, role = Role.User }, request); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(error.Code, error.Description); + } + + return BadRequest(ModelState); + } + + + [HttpPost] + [Route("login")] + public async Task> Authenticate([FromBody] AuthRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var managedUser = await _userManager.FindByEmailAsync(request.Email!); + + if (managedUser == null) + { + return BadRequest("Bad credentials"); + } + + var isPasswordValid = await _userManager.CheckPasswordAsync(managedUser, request.Password!); + + if (!isPasswordValid) + { + return BadRequest("Bad credentials"); + } + + var userInDb = _context.Users.FirstOrDefault(u => u.Email == request.Email); + + if (userInDb is null) + { + return Unauthorized(); + } + + var accessToken = _tokenService.CreateToken(userInDb); + await _context.SaveChangesAsync(); + + return Ok(new AuthResponse + { + Username = userInDb.UserName, + Email = userInDb.Email, + Token = accessToken, + }); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthRequest.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthRequest.cs new file mode 100644 index 00000000..a7fe06c3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthRequest.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.Data; + +namespace api_cinema_challenge.DTOs.Auth; + +public class AuthRequest +{ + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() + { + return true; + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthResponse.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthResponse.cs new file mode 100644 index 00000000..2507065e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthResponse.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.DTOs.Auth; + +public class AuthResponse +{ + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/RegistrationRequest.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/RegistrationRequest.cs new file mode 100644 index 00000000..266eff7d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/RegistrationRequest.cs @@ -0,0 +1,22 @@ +using api_cinema_challenge.Enums; +using System.ComponentModel.DataAnnotations; +using System.Data; + +namespace api_cinema_challenge.DTOs.Auth; + + + + +public class RegistrationRequest +{ + [Required] + public string? Email { get; set; } + + [Required] + public string? Username { get { return this.Email; } set { } } + + [Required] + public string? Password { get; set; } + + public Role Role { get; set; } = Role.User; +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerDto.cs new file mode 100644 index 00000000..ffea0115 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerDto.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.DTOs.Customer +{ + public class CustomerDto + { + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPostDto.cs new file mode 100644 index 00000000..d1927f50 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPostDto.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs.Customer +{ + public class CustomerPostDto + { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPutDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPutDto.cs new file mode 100644 index 00000000..ec0a4345 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Customer/CustomerPutDto.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs.Customer +{ + public class CustomerPutDto + { + public string? Name { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs new file mode 100644 index 00000000..ee8d83d2 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MovieDto.cs @@ -0,0 +1,15 @@ +using api_cinema_challenge.DTOs.Screening; + +namespace api_cinema_challenge.DTOs.Movie +{ + public class MovieDto + { + public int Id { get; set; } + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs new file mode 100644 index 00000000..35ab1869 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePostDto.cs @@ -0,0 +1,13 @@ +using api_cinema_challenge.DTOs.Screening; + +namespace api_cinema_challenge.DTOs.Movie +{ + public class MoviePostDto + { + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + public ICollection Screenings { get; set; } = new List(); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs new file mode 100644 index 00000000..c22c0c38 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movie/MoviePutDto.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs.Movie +{ + public class MoviePutDto + { + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningDto.cs new file mode 100644 index 00000000..82512795 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningDto.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.DTOs.Screening +{ + public class ScreeningDto + { + public int Id { get; set; } + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningPostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningPostDto.cs new file mode 100644 index 00000000..2fd735e3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Screening/ScreeningPostDto.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs.Screening +{ + public class ScreeningPostDto + { + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs new file mode 100644 index 00000000..74629d0e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketDto.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs.Ticket +{ + public class TicketDto + { + public int Id { get; set; } + public int NumSeats { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketPostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketPostDto.cs new file mode 100644 index 00000000..67c75452 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Ticket/TicketPostDto.cs @@ -0,0 +1,7 @@ +namespace api_cinema_challenge.DTOs.Ticket +{ + public class TicketPostDto + { + public int NumSeats { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index ad4fe854..e051062c 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,26 +1,58 @@ -using Microsoft.EntityFrameworkCore; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using System.Security.Claims; + namespace api_cinema_challenge.Data { - public class CinemaContext : DbContext + // IdentityUserContext instead of Db in workshop + public class CinemaContext : 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) { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .Property(u => u.Role) + .HasConversion(); + // relations + modelBuilder.Entity() + .HasOne(t => t.Customer) + .WithMany(c => c.Tickets) + .HasForeignKey(t => t.CustomerId); + + modelBuilder.Entity() + .HasOne(t => t.Screening) + .WithMany(s => s.Tickets) + .HasForeignKey(t => t.ScreeningId); + + modelBuilder.Entity() + .HasOne(s => s.Movie) + .WithMany(m => m.Screenings) + .HasForeignKey(s => s.MovieId); + + // seeder + var seeder = new Seeder(); + seeder.Seed(); + modelBuilder.Entity().HasData(seeder.Customers); + modelBuilder.Entity().HasData(seeder.Movies); + modelBuilder.Entity().HasData(seeder.Screenings); + modelBuilder.Entity().HasData(seeder.Tickets); + + } + + public DbSet Customers { get; set; } + public DbSet Movies { get; set; } + public DbSet Screenings { get; set; } + public DbSet Tickets { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs new file mode 100644 index 00000000..47a11245 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs @@ -0,0 +1,66 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Data +{ + public class Seeder + { + private List _customers = new(); + private List _movies = new(); + private List _screenings = new(); + private List _tickets = new(); + + public void Seed() + { + var customer1 = new Customer() { Id = 1, Name = "Adam", Email = "a@a.com", Phone = "111" }; + var customer2 = new Customer() { Id = 2, Name = "Blazej", Email = "b@b.com", Phone = "222" }; + var customer3 = new Customer() { Id = 3, Name = "Kristian", Email = "c@c.com", Phone = "333" }; + var customer4 = new Customer() { Id = 4, Name = "Filip", Email = "d@c.com", Phone = "444" }; + var customer5 = new Customer() { Id = 5, Name = "Damian", Email = "e@e.com", Phone = "555" }; + + var movie1 = new Movie() { Id = 1, Title = "Movie One", Rating = "PG13", Description = "fefef", RuntimeMins = 60 }; + var movie2 = new Movie() { Id = 2, Title = "Movie 2", Rating = "PG13", Description = "hrdr", RuntimeMins = 60 }; + var movie3 = new Movie() { Id = 3, Title = "333 movie", Rating = "PG13", Description = "esge", RuntimeMins = 60 }; + var movie4 = new Movie() { Id = 4, Title = "444 movie", Rating = "PG13", Description = "vesve", RuntimeMins = 60 }; + var movie5 = new Movie() { Id = 5, Title = "555 movie", Rating = "PG13", Description = "dwawd", RuntimeMins = 60 }; + + var screening1 = new Screening() { Id = 1, MovieId = 1, ScreenNumber = 1, Capacity = 50, StartsAt = DateTime.UtcNow }; + var screening2 = new Screening() { Id = 2, MovieId = 1, ScreenNumber = 1, Capacity = 50, StartsAt = DateTime.UtcNow }; + + var ticket1 = new Ticket() { Id = 1, ScreeningId = 1, CustomerId = 1, NumSeats = 1 }; + var ticket2 = new Ticket() { Id = 2, ScreeningId = 1, CustomerId = 2, NumSeats = 1 }; + var ticket3 = new Ticket() { Id = 3, ScreeningId = 1, CustomerId = 3, NumSeats = 1 }; + + var ticket4 = new Ticket() { Id = 4, ScreeningId = 2, CustomerId = 1, NumSeats = 1 }; + var ticket5 = new Ticket() { Id = 5, ScreeningId = 2, CustomerId = 2, NumSeats = 1 }; + var ticket6 = new Ticket() { Id = 6, ScreeningId = 2, CustomerId = 3, NumSeats = 1 }; + + _customers.Add(customer1); + _customers.Add(customer2); + _customers.Add(customer3); + _customers.Add(customer4); + _customers.Add(customer5); + + _movies.Add(movie1); + _movies.Add(movie2); + _movies.Add(movie3); + _movies.Add(movie4); + _movies.Add(movie5); + + _screenings.Add(screening1); + _screenings.Add(screening2); + + _tickets.Add(ticket1); + _tickets.Add(ticket2); + _tickets.Add(ticket3); + _tickets.Add(ticket4); + _tickets.Add(ticket5); + _tickets.Add(ticket6); + + } + + public List Customers { get { return _customers; } } + public List Movies { get { return _movies; } } + public List Screenings { get { return _screenings; } } + public List Tickets { get { return _tickets; } } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs new file mode 100644 index 00000000..d3e6c00e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -0,0 +1,155 @@ +using api_cinema_challenge.DTOs.Customer; +using api_cinema_challenge.DTOs.Ticket; +using api_cinema_challenge.Factories; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; +using api_cinema_challenge.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class CustomerEndpoint + { + public static void ConfigureCustomerEndpoint(this WebApplication app) + { + string groupName = "customers"; + string contentType = "application/json"; + + var customerGroup = app.MapGroup(groupName); + + customerGroup.MapGet("/", GetAllCustomers); + customerGroup.MapPost("/", CreateCustomer).Accepts(contentType); + customerGroup.MapPut("/{customerId}", UpdateCustomer).Accepts(contentType); + customerGroup.MapDelete("/{customerId}", DeleteCustomer); + + customerGroup.MapPost("/{customerId}/screenings/{screeningId}", BookTicket).Accepts(contentType); + customerGroup.MapGet("/{customerId}/screenings/{screeningId}", GetTickets); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetAllCustomers(ICustomerRepository repository) + { + var customers = await repository.GetAllAsync(); + + List dtos = new List(); + foreach (var customer in customers) + { + dtos.Add(CustomerFactory.DtoFromCustomer(customer)); + } + + return TypedResults.Ok(new { Status = "success", Data = dtos}); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + private static async Task CreateCustomer(ICustomerRepository repository, HttpRequest request) + { + CustomerPostDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest( new { status = "failure"} ); + } + + var added = await repository.CreateCustomer(CustomerFactory.CustomerFromPostDto(inDto)); + if (added is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var outDto = CustomerFactory.DtoFromCustomer(added); + + // TODO move to other class + var url = $"{request.Scheme}://{request.Host}{request.Path}/{outDto.Id}"; + + return TypedResults.Created(url, new {status = "success", data = outDto}); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task UpdateCustomer(ICustomerRepository repository, HttpRequest request, int id) + { + CustomerPutDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var entity = await repository.GetByIdAsync(id); + if (entity is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + + var updated = await repository.UpdateCustomer(CustomerFactory.CustomerFromPutDto(inDto, entity)); + if (updated is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + + return TypedResults.Created(); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task DeleteCustomer(ICustomerRepository repository, int customerId) + { + var customer = await repository.DeleteCustomer(customerId); + if (customer is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + + var dto = CustomerFactory.DtoFromCustomer(customer); + return TypedResults.Ok( new { status = "success", data = dto}); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + private static async Task BookTicket(ITicketRepository ticketRepository, HttpRequest request, int customerId, int screeningId) + { + TicketPostDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var ticket = await ticketRepository.CreateTicket(TicketFactory.TicketFromPostDto(inDto, customerId, screeningId)); + if (ticket is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var dto = TicketFactory.DtoFromTicket(ticket); + return TypedResults.Created(); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task GetTickets(ITicketRepository ticketRepository, HttpRequest request, int customerId, int screeningId) + { + var tickets = await ticketRepository.GetByIdAsync(customerId, screeningId); + if (tickets is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + + List dtos = new(); + foreach (var ticket in tickets) + { + dtos.Add(TicketFactory.DtoFromTicket(ticket)); + } + // TODO fix url + var url = $"{request.Scheme}://{request.Host}{request.Path}/"; + return TypedResults.Created(url, new { status = "success", data = dtos}); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs new file mode 100644 index 00000000..e6b86f09 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -0,0 +1,156 @@ +using api_cinema_challenge.DTOs.Customer; +using api_cinema_challenge.DTOs.Movie; +using api_cinema_challenge.DTOs.Screening; +using api_cinema_challenge.DTOs.Ticket; +using api_cinema_challenge.Factories; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; +using api_cinema_challenge.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class MovieEndpoint + { + public static void ConfigureMovieEndpoint(this WebApplication app) + { + const string groupName = "movies"; + const string contentType = "application/json"; + + var moviesGroup = app.MapGroup(groupName); + + moviesGroup.MapGet("/", GetAllMovies); + moviesGroup.MapPost("/", CreateMovie).Accepts(contentType); + moviesGroup.MapPut("/{movieId}", UpdateMovie).Accepts(contentType); + moviesGroup.MapDelete("/{movieId}", DeleteMovie); + + moviesGroup.MapPost("/{movieId}/screenings", CreateScreening).Accepts(contentType); + moviesGroup.MapGet("/{movieId}/screenings", GetScreenings); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetAllMovies(IMovieRepository repository) + { + var movie = await repository.GetAllAsync(); + + List dtos = new(); + foreach (var customer in movie) + { + dtos.Add(MovieFactory.DtoFromMovie(customer)); + } + + return TypedResults.Ok(new { Status = "success", Data = dtos }); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + private static async Task CreateMovie(IMovieRepository repository, HttpRequest request) + { + MoviePostDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var added = await repository.CreateMovie(MovieFactory.MovieFromPostDto(inDto)); + if (added is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var outDto = MovieFactory.DtoFromMovie(added); + + var url = $"{request.Scheme}://{request.Host}{request.Path}/{outDto.Id}"; + + return TypedResults.Created(url, new { status = "success", data = outDto }); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task UpdateMovie(IMovieRepository repository, HttpRequest request, int movieId) + { + MoviePutDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var entity = await repository.GetByIdAsync(movieId); + if (entity is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + + var updated = await repository.UpdateMovie(MovieFactory.MovieFromPutDto(inDto, entity)); + if (updated is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + + return TypedResults.Created(); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task DeleteMovie(IMovieRepository repository, int movieId) + { + var movie = await repository.DeleteMovie(movieId); + if (movie is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + + var dto = MovieFactory.DtoFromMovie(movie); + return TypedResults.Ok(new { status = "success", data = dto }); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + private static async Task CreateScreening(IScreeningRepository repository, HttpRequest request, int movieId) + { + ScreeningPostDto inDto = await Utility.ValidateFromRequest(request); + if (inDto is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var ticket = await repository.CreateScreening(ScreeningFactory.ScreeningFromPostDto(inDto, movieId)); + if (ticket is null) + { + return TypedResults.BadRequest(new { status = "failure" }); + } + + var dto = ScreeningFactory.DtoFromScreening(ticket); + return TypedResults.Created(); + } + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task GetScreenings(IScreeningRepository repository, HttpRequest request, int movieId) + { + var screenings = await repository.GetByIdAsync(movieId); + if (screenings is null) + { + return TypedResults.NotFound(new { status = "failure" }); + } + + List dtos = new(); + foreach (var screening in screenings) + { + dtos.Add(ScreeningFactory.DtoFromScreening(screening)); + } + // TODO fix url + var url = $"{request.Scheme}://{request.Host}{request.Path}/"; + return TypedResults.Created(url, new { status = "success", data = dtos }); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs new file mode 100644 index 00000000..3240a249 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Enums/Role.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.Enums +{ + public enum Role + { + User, + Admin + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs new file mode 100644 index 00000000..67d7e231 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Factories/CustomerFactory.cs @@ -0,0 +1,46 @@ +using api_cinema_challenge.DTOs.Customer; +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Factories +{ + public static class CustomerFactory + { + public static CustomerDto DtoFromCustomer(Customer customer) + { + var dto = new CustomerDto(); + + dto.Id = customer.Id; + dto.Name = customer.Name; + dto.Email = customer.Email; + dto.Phone = customer.Phone; + dto.CreatedAt = customer.CreatedAt; + dto.UpdatedAt = customer.UpdatedAt; + + return dto; + } + + public static Customer CustomerFromPostDto(CustomerPostDto dto) + { + var customer = new Customer(); + + customer.Name = dto.Name; + customer.Email = dto.Email; + customer.Phone = dto.Phone; + customer.CreatedAt = DateTime.UtcNow; + customer.UpdatedAt = DateTime.UtcNow; + + return customer; + } + + public static Customer CustomerFromPutDto(CustomerPutDto dto, Customer oldCustomer) + { + var updated = oldCustomer; + + if (dto.Name is not null) updated.Name = dto.Name; + if (dto.Email is not null) updated.Email = dto.Email; + if (dto.Phone is not null) updated.Phone = dto.Phone; + + return updated; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs new file mode 100644 index 00000000..53eed04f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Factories/MovieFactory.cs @@ -0,0 +1,48 @@ +using api_cinema_challenge.DTOs.Movie; +using api_cinema_challenge.Models; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; + +namespace api_cinema_challenge.Factories +{ + public static class MovieFactory + { + public static MovieDto DtoFromMovie(Movie movie) + { + var dto = new MovieDto(); + + dto.Id = movie.Id; + dto.Title = movie.Title; + dto.Rating = movie.Rating; + dto.Description = movie.Description; + dto.RuntimeMins = movie.RuntimeMins; + dto.CreatedAt = movie.CreatedAt; + dto.UpdatedAt = movie.UpdatedAt; + + return dto; + } + + public static Movie MovieFromPostDto(MoviePostDto dto) + { + var movie = new Movie(); + + movie.Title = dto.Title; + movie.Rating = dto.Rating; + movie.Description = dto.Description; + movie.RuntimeMins = dto.RuntimeMins; + + return movie; + } + + public static Movie MovieFromPutDto(MoviePutDto dto, Movie oldMovie) + { + var updated = oldMovie; + + if (dto.Title is not null) updated.Title = dto.Title; + if (dto.Rating is not null) updated.Rating = dto.Rating; + if (dto.Description is not null) updated.Description = dto.Description; + if (dto.RuntimeMins != 0) updated.RuntimeMins = dto.RuntimeMins; + + return updated; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs new file mode 100644 index 00000000..0934372b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Factories/ResponseFactory.cs @@ -0,0 +1,15 @@ +namespace api_cinema_challenge.Factories +{ + public static class ResponseFactory + { + public static Object Failure() + { + return new { status = "failure"}; + } + + public static Object Success() + { + return new { status = "success" }; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs new file mode 100644 index 00000000..8eaf28b9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Factories/ScreeningFactory.cs @@ -0,0 +1,36 @@ +using api_cinema_challenge.DTOs.Screening; +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Factories +{ + public static class ScreeningFactory + { + public static Screening ScreeningFromPostDto(ScreeningPostDto dtp, int movieId) + { + var screening = new Screening(); + + screening.MovieId = movieId; + screening.ScreenNumber = dtp.ScreenNumber; + screening.Capacity = dtp.Capacity; + screening.StartsAt = dtp.StartsAt; + screening.CreatedAt = DateTime.UtcNow; + screening.UpdatedAt = DateTime.UtcNow; + + return screening; + } + + public static ScreeningDto DtoFromScreening(Screening screening) + { + var dto = new ScreeningDto(); + + dto.Id = screening.Id; + dto.ScreenNumber = screening.ScreenNumber; + dto.Capacity = screening.Capacity; + dto.StartsAt = screening.StartsAt; + dto.CreatedAt = DateTime.UtcNow; + dto.UpdatedAt = DateTime.UtcNow; + + return dto; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Factories/TicketFactory.cs b/api-cinema-challenge/api-cinema-challenge/Factories/TicketFactory.cs new file mode 100644 index 00000000..2f4f0617 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Factories/TicketFactory.cs @@ -0,0 +1,34 @@ +using api_cinema_challenge.DTOs.Ticket; +using api_cinema_challenge.Models; +using Microsoft.AspNetCore.StaticAssets; + +namespace api_cinema_challenge.Factories +{ + public static class TicketFactory + { + public static Ticket TicketFromPostDto(TicketPostDto dto, int customerId, int screeningId) + { + var ticket = new Ticket(); + + ticket.CustomerId = customerId; + ticket.ScreeningId = screeningId; + ticket.NumSeats = dto.NumSeats; + ticket.CreatedAt = DateTime.UtcNow; + ticket.UpdatedAt = DateTime.UtcNow; + + return ticket; + } + + public static TicketDto DtoFromTicket(Ticket ticket) + { + var dto = new TicketDto(); + + dto.Id = ticket.Id; + dto.NumSeats = ticket.NumSeats; + dto.CreatedAt = ticket.CreatedAt; + dto.UpdatedAt = ticket.UpdatedAt; + + return dto; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs new file mode 100644 index 00000000..b07488a8 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,540 @@ +// +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") + .IsRequired() + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .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(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Email = "a@a.com", + Name = "Adam", + Phone = "111", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Email = "b@b.com", + Name = "Blazej", + Phone = "222", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Email = "c@c.com", + Name = "Kristian", + Phone = "333", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Email = "d@c.com", + Name = "Filip", + Phone = "444", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Email = "e@e.com", + Name = "Damian", + Phone = "555", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + 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(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "fefef", + Rating = "PG13", + RuntimeMins = 60, + Title = "Movie One", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "hrdr", + Rating = "PG13", + RuntimeMins = 60, + Title = "Movie 2", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "esge", + Rating = "PG13", + RuntimeMins = 60, + Title = "333 movie", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "vesve", + Rating = "PG13", + RuntimeMins = 60, + Title = "444 movie", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "dwawd", + Rating = "PG13", + RuntimeMins = 60, + Title = "555 movie", + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + + b.HasData( + new + { + Id = 1, + Capacity = 50, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 1, + ScreenNumber = 1, + StartsAt = new DateTime(2025, 8, 25, 10, 49, 52, 159, DateTimeKind.Utc).AddTicks(3594), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + Capacity = 50, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + MovieId = 1, + ScreenNumber = 1, + StartsAt = new DateTime(2025, 8, 25, 10, 49, 52, 159, DateTimeKind.Utc).AddTicks(3695), + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("Tickets"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 1, + NumSeats = 1, + ScreeningId = 1, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 2, + NumSeats = 1, + ScreeningId = 1, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 3, + NumSeats = 1, + ScreeningId = 1, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 1, + NumSeats = 1, + ScreeningId = 2, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 2, + NumSeats = 1, + ScreeningId = 2, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 6, + CreatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + CustomerId = 3, + NumSeats = 1, + ScreeningId = 2, + UpdatedAt = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("api_cinema_challenge.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.HasOne("api_cinema_challenge.Models.Movie", "Movie") + .WithMany("Screenings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.HasOne("api_cinema_challenge.Models.Customer", "Customer") + .WithMany("Tickets") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany("Tickets") + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs new file mode 100644 index 00000000..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..7f73d7a6 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,13 @@ +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 ICollection Tickets { get; set; } = new List(); + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs new file mode 100644 index 00000000..43f5e4ac --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,14 @@ +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 ICollection Screenings { get; set; } = new List(); + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs new file mode 100644 index 00000000..5f12a8e7 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,15 @@ +namespace api_cinema_challenge.Models +{ + public class Screening + { + public int Id { get; set; } + public int MovieId { get; set; } + public Movie Movie { get; set; } + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + public ICollection Tickets { get; set; } = new List(); + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs new file mode 100644 index 00000000..9e93413d --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,14 @@ +namespace api_cinema_challenge.Models +{ + public class Ticket + { + public int Id { get; set; } + public int CustomerId { get; set; } + public Customer Customer { get; set; } + public int ScreeningId { get; set; } + public Screening Screening { get; set; } + public int NumSeats { get; set; } + 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..9e7a457d 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,20 +1,160 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using api_cinema_challenge.Repository.Interfaces; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using System.Diagnostics; +using System.Text; +using System.Text.Json.Serialization; 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.AddRouting(options => options.LowercaseUrls = true); + +builder.Services.AddDbContext(options => { + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")) + .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); + options.LogTo(message => Debug.WriteLine(message)); + options.EnableSensitiveDataLogging(); +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +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) + ), + }; + }); + +// policy-based authorization for Admin and User roles +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("Admin", policy => + policy.RequireRole("Admin")); + options.AddPolicy("User", policy => + policy.RequireRole("User")); +}); + + +// Build the app var app = builder.Build(); -// Configure the HTTP request pipeline. +// Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Test API v1"); + options.RoutePrefix = "swagger"; + }); + } +app.UseSwagger(); +app.UseSwaggerUI(options => +{ + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Test API v1"); + options.RoutePrefix = "swagger"; +}); app.UseHttpsRedirection(); +app.UseStatusCodePages(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.ConfigureCustomerEndpoint(); +app.ConfigureMovieEndpoint(); + +app.MapControllers(); app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs new file mode 100644 index 00000000..48cce0bb --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs @@ -0,0 +1,67 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository +{ + public class CustomerRepository : ICustomerRepository + { + private CinemaContext _db; + + public CustomerRepository(CinemaContext db) + { + _db = db; + } + + public async Task CreateCustomer(Customer customer) + { + var exists = _db.Customers.Where(c => c.Id == customer.Id).Any(); + if (exists) return null; + + await _db.Customers.AddAsync(customer); + await _db.SaveChangesAsync(); + + return customer; + } + + public async Task DeleteCustomer(int id) + { + var exists = await _db.Customers.AnyAsync(x => x.Id == id); + if (!exists) + { + return null; + } + + var entity = await _db.Customers.FirstAsync(x => x.Id == id); + _db.Customers.Remove(entity); + await _db.SaveChangesAsync(); + + return entity; + } + + public async Task> GetAllAsync() + { + return await _db.Customers.ToListAsync(); + } + + public async Task GetByIdAsync(int id) + { + var entity = await _db.Customers.Where(c => c.Id == id).FirstOrDefaultAsync(); + if (entity is null) + { + return null; + } + + return entity; + } + + public async Task UpdateCustomer(Customer customer) + { + _db.Customers.Update(customer); + await _db.SaveChangesAsync(); + + return customer; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs new file mode 100644 index 00000000..016b4b85 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ICustomerRepository.cs @@ -0,0 +1,13 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository.Interfaces +{ + public interface ICustomerRepository + { + public Task GetByIdAsync(int id); + public Task> GetAllAsync(); + public Task CreateCustomer(Customer customer); + public Task UpdateCustomer(Customer customer); + public Task DeleteCustomer(int id); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs new file mode 100644 index 00000000..767f7e55 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IMovieRepository.cs @@ -0,0 +1,13 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository.Interfaces +{ + public interface IMovieRepository + { + public Task GetByIdAsync(int id); + public Task> GetAllAsync(); + public Task CreateMovie(Movie movie); + public Task UpdateMovie(Movie customer); + public Task DeleteMovie(int id); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs new file mode 100644 index 00000000..78f8009e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/IScreeningRepository.cs @@ -0,0 +1,10 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository.Interfaces +{ + public interface IScreeningRepository + { + public Task> GetByIdAsync(int movieId); + public Task CreateScreening(Screening screening); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs new file mode 100644 index 00000000..49483f04 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Interfaces/ITicketRepository.cs @@ -0,0 +1,10 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository.Interfaces +{ + public interface ITicketRepository + { + public Task> GetByIdAsync(int customerId, int screeningId); + public Task CreateTicket(Ticket ticket); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs new file mode 100644 index 00000000..24d7fa30 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs @@ -0,0 +1,41 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; + +namespace api_cinema_challenge.Repository +{ + public class MovieRepository : IMovieRepository + { + private CinemaContext _db; + + public MovieRepository(CinemaContext db) + { + _db = db; + } + + public Task CreateMovie(Movie movie) + { + throw new NotImplementedException(); + } + + public Task DeleteMovie(int id) + { + throw new NotImplementedException(); + } + + public Task> GetAllAsync() + { + throw new NotImplementedException(); + } + + public Task GetByIdAsync(int id) + { + throw new NotImplementedException(); + } + + public Task UpdateMovie(Movie customer) + { + throw new NotImplementedException(); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/ScreeningRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/ScreeningRepository.cs new file mode 100644 index 00000000..1a577fbe --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/ScreeningRepository.cs @@ -0,0 +1,48 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; +using Microsoft.EntityFrameworkCore; +using System.Net.Sockets; + +namespace api_cinema_challenge.Repository +{ + public class ScreeningRepository : IScreeningRepository + { + private CinemaContext _db; + + public ScreeningRepository(CinemaContext db) + { + _db = db; + } + + public async Task CreateScreening(Screening screening) + { + var exists = await _db.Screenings + .Where(s => s.Id == screening.Id) + .AnyAsync(); + + if (exists) return null; + + await _db.Screenings.AddAsync(screening); + await _db.SaveChangesAsync(); + + return screening; + } + + public async Task> GetByIdAsync(int movieId) + { + bool exists = await _db.Screenings + .Where(s => s.MovieId == movieId) + .AnyAsync(); + + if (!exists) + return null; + + var screenings = await _db.Screenings + .Where(s => s.MovieId == movieId) + .ToListAsync(); + + return screenings; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs new file mode 100644 index 00000000..a69c6d77 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/TicketRepository.cs @@ -0,0 +1,51 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository.Interfaces; +using Microsoft.EntityFrameworkCore; +using System.Net.Sockets; + +namespace api_cinema_challenge.Repository +{ + public class TicketRepository : ITicketRepository + { + private CinemaContext _db; + + public TicketRepository(CinemaContext db) + { + _db = db; + } + + public async Task CreateTicket(Ticket ticket) + { + var exists = await _db.Tickets + .Where(t => t.CustomerId == ticket.CustomerId) + .Where(t => t.ScreeningId == ticket.ScreeningId) + .AnyAsync(); + + if (exists) return null; + + await _db.Tickets.AddAsync(ticket); + await _db.SaveChangesAsync(); + + return ticket; + } + + public async Task> GetByIdAsync(int customerId, int screeningId) + { + bool exists = await _db.Tickets + .Where(t => t.CustomerId == customerId) + .Where(t => t.ScreeningId == screeningId) + .AnyAsync(); + + if (!exists) + return null; + + var tickets = await _db.Tickets + .Where(t => t.CustomerId == customerId) + .Where(t => t.ScreeningId==screeningId) + .ToListAsync(); + + return tickets; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs new file mode 100644 index 00000000..ed9f8dc9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs @@ -0,0 +1,82 @@ +namespace api_cinema_challenge.Services; + +using api_cinema_challenge.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +public class TokenService +{ + private const int ExpirationMinutes = 60; + private readonly ILogger _logger; + public TokenService(ILogger logger) + { + _logger = logger; + } + + public string CreateToken(ApplicationUser user) + { + + var expiration = DateTime.UtcNow.AddMinutes(ExpirationMinutes); + var token = CreateJwtToken( + CreateClaims(user), + CreateSigningCredentials(), + expiration + ); + var tokenHandler = new JwtSecurityTokenHandler(); + + _logger.LogInformation("JWT Token created"); + + return tokenHandler.WriteToken(token); + } + + private JwtSecurityToken CreateJwtToken(List claims, SigningCredentials credentials, + DateTime expiration) => + new( + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidIssuer"], + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidAudience"], + claims, + expires: expiration, + signingCredentials: credentials + ); + + private List CreateClaims(ApplicationUser user) + { + var jwtSub = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["JwtRegisteredClaimNamesSub"]; + + try + { + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, jwtSub), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()) + }; + + return claims; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + private SigningCredentials CreateSigningCredentials() + { + var symmetricSecurityKey = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["SymmetricSecurityKey"]; + + return new SigningCredentials( + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + SecurityAlgorithms.HmacSha256 + ); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Utils/Utility.cs b/api-cinema-challenge/api-cinema-challenge/Utils/Utility.cs new file mode 100644 index 00000000..66eed50f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Utils/Utility.cs @@ -0,0 +1,26 @@ +using System.Text.Json; + +namespace api_cinema_challenge.Utils +{ + public static class Utility + { + public static async Task ValidateFromRequest(HttpRequest request) + { + T? entity; + try + { + entity = await request.ReadFromJsonAsync(); + } + catch (JsonException ex) + { + return default; + } + catch (Exception ex) + { + return default; + } + + return entity; + } + } +} 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..dc9602ea 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -1,20 +1,16 @@ - + net9.0 enable enable api_cinema_challenge + 8499e9e9-9306-422f-a58d-332dfc8c5416 - - - - - - - + + @@ -24,11 +20,8 @@ + - - - - diff --git a/api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt b/api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt new file mode 100644 index 00000000..17673d93 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/jwt appsettings.txt @@ -0,0 +1,11 @@ +"SiteSettings": { + "AdminEmail": "example@test.com", + "AdminPassword": "administrator" +}, + +"JwtTokenSettings": { + "ValidIssuer": "ExampleIssuer", + "ValidAudience": "ExampleAudience", + "SymmetricSecurityKey": "v89h3bh89vh9ve8hc89nv98nn899cnccn998ev80vi809jberh89b", + "JwtRegisteredClaimNamesSub": "rbveer3h535nn3n35nyny5umbbt" +}, \ No newline at end of file