diff --git a/api-cinema-challenge/api-cinema-challenge/Controller/UserController.cs b/api-cinema-challenge/api-cinema-challenge/Controller/UserController.cs new file mode 100644 index 00000000..873ed03a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Controller/UserController.cs @@ -0,0 +1,100 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.DTOs.Requests; +using api_cinema_challenge.DTOs.Response; +using api_cinema_challenge.Enums; +using api_cinema_challenge.Models; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Controller +{ + [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, + }); + } + } + +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs new file mode 100644 index 00000000..9857c448 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGet.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.DTOs +{ + public class CustomerGet + { + 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/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/MovieGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGet.cs new file mode 100644 index 00000000..96c89222 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGet.cs @@ -0,0 +1,15 @@ +namespace api_cinema_challenge.DTOs +{ + public class MovieGet + { + 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/MoviePost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePost.cs new file mode 100644 index 00000000..759d4659 --- /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/Requests/AuthRequest.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/AuthRequest.cs new file mode 100644 index 00000000..c269f7e8 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/AuthRequest.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs.Requests +{ + public class AuthRequest + { + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() { return true; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/RegistrationRequest.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/RegistrationRequest.cs new file mode 100644 index 00000000..c0a850b2 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Requests/RegistrationRequest.cs @@ -0,0 +1,19 @@ +using api_cinema_challenge.Enums; +using System.ComponentModel.DataAnnotations; + +namespace api_cinema_challenge.DTOs.Requests +{ + public class RegistrationRequest + { + [Required] + public string? Email { get; set; } + + [Required] + public string? Username { get { return this.Email; } set { } } + + [Required] + public string? Password { get; set; } + + public Role Role { get; set; } = Role.User; + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Response/AuthResponse.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Response/AuthResponse.cs new file mode 100644 index 00000000..016e3288 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Response/AuthResponse.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs.Response +{ + public class AuthResponse + { + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs new file mode 100644 index 00000000..74da8cd6 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGet.cs @@ -0,0 +1,14 @@ +namespace api_cinema_challenge.DTOs +{ + public class ScreeningGet + { + public int MovieId { 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/ScreeningPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs new file mode 100644 index 00000000..b02a2b97 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPost.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs +{ + public class ScreeningPost + { + //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/TicketGet.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketGet.cs new file mode 100644 index 00000000..ec1fed89 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketGet.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.DTOs +{ + public class TicketGet + { + 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/TicketPost.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketPost.cs new file mode 100644 index 00000000..b05eccb3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketPost.cs @@ -0,0 +1,7 @@ +namespace api_cinema_challenge.DTOs +{ + public class TicketPost + { + public int NumSeats { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index ad4fe854..cce2bdef 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,5 +1,8 @@ -using Microsoft.EntityFrameworkCore; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; namespace api_cinema_challenge.Data { @@ -20,7 +23,34 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { + //modelBuilder.Entity().HasMany(t => t.Tickets); + /*modelBuilder.Entity() + .HasMany(c => c.Tickets) + .WithOne(c => c.Customer);*/ + modelBuilder.Entity() + .HasOne(t => t.Screening) + .WithMany(t => t.Tickets) + .HasForeignKey(t => t.ScreeningId); + + modelBuilder.Entity() + .HasOne(t => t.Customer) + .WithMany(t => t.Tickets) + .HasForeignKey(t => t.CustomerId); + + modelBuilder.Entity() + .HasOne(s => s.Movie) + .WithMany(s => s.Screenings) + .HasForeignKey(s => s.MovieId); } + public DbSet Tickets { get; set; } + public DbSet Users { get; set; } + public DbSet Screenings { get; set; } + public DbSet Customers { get; set; } + public DbSet Movies { get; set; } + + + //public DbSet Ticket { get; set; } + //publi } } 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..7c5c652b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -0,0 +1,92 @@ +//using Microsoft.AspNetCore.Mvc; +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Endpoints +{ + public static class CustomerEndpoint + { + public static void ConfigureCustomerEndpoints(this WebApplication app) + { + var customer = app.MapGroup("customers"); + + customer.MapGet("/", GetAll); + customer.MapPost("/", Create); + customer.MapPut("/{id}", Update); + customer.MapDelete("/{id}", Delete); + + } + + private static CustomerGet CustomerToCustomerGet(Customer c) + { + CustomerGet customerShow = new CustomerGet() {Id = c.Id, Name = c.Name, Email = c.Email, Phone = c.Phone, CreatedAt = c.CreatedAt, UpdatedAt = c.UpdatedAt }; + return customerShow; + } + + [Authorize(Roles = "Admin,User")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository customerRepo) { + List response = new List(); + var results = await customerRepo.GetAll(); + foreach (Customer c in results) { + CustomerGet customerShow = CustomerToCustomerGet(c); + response.Add(customerShow); + } + return TypedResults.Ok(response); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task Create(IRepository customerRepo, CustomerPost cModel) + { + DateTime time = DateTime.UtcNow.ToUniversalTime(); + Customer newCustomer = new Customer() { + Name = cModel.Name, + Email = cModel.Email, + Phone = cModel.Phone, + CreatedAt = time, + UpdatedAt = time + }; + await customerRepo.Insert(newCustomer); + return TypedResults.Created($"Created object with id: {newCustomer.Id}"); + } + + [Authorize(Roles = "Admin,User")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public static async Task Update(IRepository customerRepo, int id, CustomerPost cModel) + { + Customer? cTarget = await customerRepo.GetById(id); + if(cTarget!= null) + { + DateTime UpdatedTime = DateTime.UtcNow.ToUniversalTime(); + cTarget.Name = cModel.Name; + cTarget.Email = cModel.Email; + cTarget.Phone = cModel.Phone; + cTarget.UpdatedAt = UpdatedTime; + + await customerRepo.Update(cTarget); + return TypedResults.Created($"Updated object with id: {id}"); + } + return TypedResults.NotFound(); + } + + [Authorize(Roles = "Admin,User")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public static async Task Delete(IRepository customerRepo, int id) + { + Customer? cTarget = await customerRepo.GetById(id); + if (cTarget != null) + { + await customerRepo.Delete(id); + return TypedResults.Ok(CustomerToCustomerGet(cTarget)); + } + return TypedResults.NotFound(); + } + } +} 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..3dacd158 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -0,0 +1,103 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Runtime.InteropServices; + +namespace api_cinema_challenge.Endpoints +{ + public static class MovieEndpoint + { + public static void ConfigureMovieEndpoints(this WebApplication app) + { + var movie = app.MapGroup("movies"); + + movie.MapGet("/", GetAll); + movie.MapPost("/", Create); + movie.MapPut("/{id}", Update); + movie.MapDelete("/{id}", Delete); + + } + + public static MovieGet MovieToMovieGet(Movie m) + { + MovieGet movieShow = new MovieGet() { Id = m.Id, Title = m.Title, Rating = m.Rating, Description = m.Description, RunTimeMins = m.RuntimeMins, CreatedAt = m.CreatedAt, UpdatedAt = m.UpdatedAt }; + return movieShow; + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository movieRepo) + { + List response = new List(); + var results = await movieRepo.GetAll(); + foreach (Movie m in results) + { + MovieGet movieShow = MovieToMovieGet(m); + response.Add(movieShow); + } + return TypedResults.Ok(response); + } + + [Authorize(Roles = "Admin,User")] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task Create(IRepository movieRepo, MoviePost mModel) + { + DateTime time = DateTime.UtcNow.ToUniversalTime(); + Movie newMovie = new Movie() + { + Title = mModel.Title, + Rating = mModel.Rating, + Description = mModel.Description, + RuntimeMins = mModel.RunTimeMins, + CreatedAt = time, + UpdatedAt = time + }; + await movieRepo.Insert(newMovie); + return TypedResults.Created($"Created object with id: {newMovie.Id}"); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public static async Task Update(IRepository movieRepo, int id, MoviePost mModel) + { + Movie? mTarget = await movieRepo.GetById(id); + + if (mTarget != null) + { + DateTime time = DateTime.UtcNow.ToUniversalTime(); + mTarget.Title = mModel.Title; + mTarget.Rating = mModel.Rating; + mTarget.Description = mModel.Description; + mTarget.RuntimeMins = mModel.RunTimeMins; + mTarget.UpdatedAt = time; + + await movieRepo.Update(mTarget); + return TypedResults.Created($"Updated object with id: {id}"); + } + return TypedResults.NotFound(); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + + public static async Task Delete(IRepository movieRepo, int id) + { + Movie? mTarget = await movieRepo.GetById(id); + if (mTarget != null) + { + await movieRepo.Delete(id); + return TypedResults.Ok(MovieToMovieGet(mTarget)); + } + return TypedResults.NotFound(); + } + + } +} + diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs new file mode 100644 index 00000000..3a2a986f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs @@ -0,0 +1,76 @@ +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 ScreeningEndpoint + { + public static void ConfigureScreeningEndpoints(this WebApplication app) + { + var screening = app.MapGroup("movies/"); + + screening.MapGet("{id}/screenings", GetAll); + screening.MapPost("{id}/screenings", Create); + } + + public static ScreeningGet ScreenToScreeningGet(Screening s) + { + ScreeningGet screenShow = new ScreeningGet() { MovieId = s.MovieId, ScreenNumber = s.ScreenNumber, Capacity = s.Capacity, StartsAt = s.StartsAt, CreatedAt = s.CreatedAt, UpdatedAt = s.UpdatedAt }; + return screenShow; + } + + [Authorize(Roles = "Admin,User")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository screenRepo, int id) + { + List response = new List(); + var all_results = await screenRepo.GetAll(); + var results = all_results.Where(s => s.MovieId == id); + foreach (Screening s in results) + { + ScreeningGet screenShow = ScreenToScreeningGet(s); + response.Add(screenShow); + } + return TypedResults.Ok(response); + } + + /* + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository screenRepo, int id) + { + List response = new List(); + var movie = await screenRepo.GetWithIncludes(m => m.Screenings); + var results = await screenRepo.GetAll(); + foreach (Screening s in results) + { + if (s.MovieId == id) + { + ScreeningGet screenShow = ScreenToScreeningGet(s); + response.Add(screenShow); + } + } + return TypedResults.Ok(response); + } + */ + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task Create(IRepository screenRepo, ScreeningPost sModel, int id) + { + DateTime time = DateTime.UtcNow.ToUniversalTime(); + Screening newScreen = new Screening() + { + MovieId = id, + ScreenNumber = sModel.ScreenNumber, + Capacity = sModel.Capacity, + StartsAt = sModel.StartsAt, + CreatedAt = time, + UpdatedAt = time + }; + await screenRepo.Insert(newScreen); + return TypedResults.Created($"Created object with id: {newScreen.Id}"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs new file mode 100644 index 00000000..32fb8ffa --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoint.cs @@ -0,0 +1,56 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace api_cinema_challenge.Endpoints +{ + public static class TicketEndpoint + { + public static void ConfigureTicketEndpoints(this WebApplication app) + { + var ticket = app.MapGroup("customers/"); + + ticket.MapGet("{cId}/screenings{sId}", GetAll); + ticket.MapPost("{cId}/screenings{sId}", Create); + } + + private static TicketGet TicketToTicketGet(Ticket t) + { + TicketGet ticketGet = new TicketGet() { Id = t.Id, NumSeats = t.NumSeats, CreatedAt = t.CreatedAt, UpdatedAt = t.UpdatedAt }; + return ticketGet; + } + + [Authorize("Admin,User")] + public static async Task GetAll(IRepository ticketRepo, int cId, int sId) + { + //List results = new List(); + var tickets = await ticketRepo.GetAll(); + var results = tickets.Where(t => t.CustomerId == cId && t.ScreeningId == sId).ToList(); + var resultsShow = new List(); + foreach (var result in results) + { + resultsShow.Add(TicketToTicketGet(result)); + } + return TypedResults.Ok(resultsShow); + } + + [Authorize(Roles = "User")] + public static async Task Create(IRepository ticketRepo, int cId, int sId, TicketPost t) + { + DateTime time = DateTime.UtcNow.ToUniversalTime(); + + Ticket ticket = new Ticket() + { + NumSeats = t.NumSeats, + CustomerId = cId, + ScreeningId = sId, + CreatedAt = time, + UpdatedAt = time + }; + await ticketRepo.Insert(ticket); + return TypedResults.Ok(TicketToTicketGet(ticket)); + } + } +} 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/ClaimsPrincipalHelpers.cs b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelpers.cs new file mode 100644 index 00000000..b1e437f5 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelpers.cs @@ -0,0 +1,31 @@ +using System.Security.Claims; +using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; + +namespace api_cinema_challenge.Helpers +{ + public static class ClaimsPrincipalHelper + { + public static string? UserId(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.NameIdentifier); + return claim?.Value; + } + public static string? Email(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.Email); + return claim?.Value; + } + + // public static string? UserId(this IIdentity identity) + // { + // if (identity != null && identity.IsAuthenticated) + // { + // // return Guid.Parse(((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value); + // return ((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value; + // } + // return null; + // } + } +} + diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.Designer.cs new file mode 100644 index 00000000..023e98fb --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.Designer.cs @@ -0,0 +1,279 @@ +// +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("20250826082751_Jwt")] + partial class Jwt + { + /// + 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.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + 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") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + 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") + .HasColumnName("createdat"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdat"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("runtimemins"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdat"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screenNumber"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("startsat"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdat"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer") + .HasColumnName("numseats"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.HasOne("api_cinema_challenge.Models.Movie", "Movie") + .WithMany("Screenings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.HasOne("api_cinema_challenge.Models.Customer", "Customer") + .WithMany("Tickets") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany("Tickets") + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.cs new file mode 100644 index 00000000..fa4c8913 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250826082751_Jwt.cs @@ -0,0 +1,164 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class Jwt : 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: "Users", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Role = table.Column(type: "integer", nullable: false), + UserName = table.Column(type: "text", nullable: true), + NormalizedUserName = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: true), + NormalizedEmail = table.Column(type: "text", 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_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Screenings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + screenNumber = table.Column(type: "integer", nullable: false), + capacity = table.Column(type: "integer", nullable: false), + startsat = table.Column(type: "timestamp with time zone", nullable: false), + createdat = table.Column(type: "timestamp with time zone", nullable: false), + updatedat = table.Column(type: "timestamp with time zone", nullable: false), + MovieId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Screenings", x => x.Id); + table.ForeignKey( + name: "FK_Screenings_Movies_MovieId", + column: x => x.MovieId, + principalTable: "Movies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + numseats = table.Column(type: "integer", nullable: false), + CustomerId = table.Column(type: "integer", nullable: false), + ScreeningId = table.Column(type: "integer", nullable: false), + createdat = table.Column(type: "timestamp with time zone", nullable: false), + updatedat = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => x.Id); + table.ForeignKey( + name: "FK_Tickets_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Tickets_Screenings_ScreeningId", + column: x => x.ScreeningId, + principalTable: "Screenings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Screenings_MovieId", + table: "Screenings", + column: "MovieId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_CustomerId", + table: "Tickets", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_ScreeningId", + table: "Tickets", + column: "ScreeningId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Tickets"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Screenings"); + + migrationBuilder.DropTable( + name: "Movies"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs new file mode 100644 index 00000000..053c9876 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,276 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + partial class CinemaContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api_cinema_challenge.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + 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") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + 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") + .HasColumnName("createdat"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdat"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("runtimemins"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdat"); + + b.Property("MovieId") + .HasColumnType("integer"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screenNumber"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("startsat"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdat"); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("NumSeats") + .HasColumnType("integer") + .HasColumnName("numseats"); + + b.Property("ScreeningId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.HasOne("api_cinema_challenge.Models.Movie", "Movie") + .WithMany("Screenings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.HasOne("api_cinema_challenge.Models.Customer", "Customer") + .WithMany("Tickets") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany("Tickets") + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs new file mode 100644 index 00000000..8e7e4061 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs @@ -0,0 +1,12 @@ +using api_cinema_challenge.Enums; +using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("Users")] + 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..5212a2f3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Net.Mail; +using Microsoft.AspNetCore.Mvc; + + +namespace api_cinema_challenge.Models +{ + [Table("Customers")] + public class Customer + { + [Key] + public int Id { get; set; } + + [Column("name")] + public string Name { get; set; } + + [Column("email")] + public string Email { get; set; } + + [Column("phone")] + public string Phone { get; set; } + + [Column("createdat")] + public DateTime CreatedAt { get; set; } + + [Column("updatedat")] + public DateTime UpdatedAt { get; set; } + + public IEnumerable Tickets { get; set; }= new List(); + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs new file mode 100644 index 00000000..d6ebf939 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("Movies")] + public class Movie + { + [Key] + public int Id { get; set; } + + [Column("title")] + public string Title { get; set; } + + [Column("rating")] + public string Rating { get; set; } + + [Column("description")] + public string Description { get; set; } + + [Column("runtimemins")] + public int RuntimeMins { get; set; } + + [Column("createdat")] + public DateTime CreatedAt { get; set; } + + [Column("updatedat")] + public DateTime UpdatedAt { get; set; } + + public List Screenings { get; set; } = new List(); + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs new file mode 100644 index 00000000..c648a929 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("Screenings")] + public class Screening + { + [Key] + public int Id { get; set; } + + [Column("screenNumber")] + public int ScreenNumber { get; set; } + + [Column("capacity")] + public int Capacity { get; set; } + + [Column("startsat")] + public DateTime StartsAt { get; set; } + + [Column("createdat")] + public DateTime CreatedAt { get; set; } + + [Column("updatedat")] + public DateTime UpdatedAt { get; set; } + + [ForeignKey("Movies")] + public int MovieId { get; set; } + + public Movie Movie { get; set; } + + public List Tickets { get; set; } = new List(); + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs new file mode 100644 index 00000000..ac387ed0 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("Tickets")] + public class Ticket + { + [Key] + public int Id { get; set; } + + [Column("numseats")] + public int NumSeats { get; set; } + + [ForeignKey("Customers")] + public int CustomerId { get; set; } + + public Customer Customer { get; set; } + + [ForeignKey("Screenings")] + public int ScreeningId { get; set; } + + public Screening Screening { get; set; } + + [Column("createdat")] + public DateTime CreatedAt { get; set; } + + [Column("updatedat")] + 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..369648cc 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,20 +1,145 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +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. +builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); + +builder.Services.AddSwaggerGen(option => +{ + option.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); + option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter a valid token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + option.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); +}); + +builder.Services.AddProblemDetails(); +builder.Services.AddApiVersioning(cfg => +{ + cfg.DefaultApiVersion = new ApiVersion(1, 1); + cfg.AssumeDefaultVersionWhenUnspecified = true; +}); +builder.Services.AddRouting(options => options.LowercaseUrls = true); +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); + options.LogTo(message => Debug.WriteLine(message)); +}); + builder.Services.AddDbContext(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped(); + +builder.Services.AddControllers().AddJsonOptions(opt => +{ + opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); + +// Specify identity requirements +// Must be added before .AddAuthentication otherwise a 404 is thrown on authorized endpoints +builder.Services + .AddIdentity(options => + { + options.SignIn.RequireConfirmedAccount = false; + options.User.RequireUniqueEmail = true; + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + }) + .AddRoles() + .AddEntityFrameworkStores(); + +// These will eventually be moved to a secrets file, but for alpha development appsettings is fine +var validIssuer = builder.Configuration.GetValue("JwtTokenSettings:ValidIssuer"); +var validAudience = builder.Configuration.GetValue("JwtTokenSettings:ValidAudience"); +var symmetricSecurityKey = builder.Configuration.GetValue("JwtTokenSettings:SymmetricSecurityKey"); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +}) + .AddJwtBearer(options => + { + options.IncludeErrorDetails = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ClockSkew = TimeSpan.Zero, + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = validIssuer, + ValidAudience = validAudience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + }; + }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { + //app.MapOpenApi(); app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); +app.UseStatusCodePages(); + + +app.UseAuthentication(); +app.UseAuthorization(); + +app.ConfigureCustomerEndpoints(); +app.ConfigureMovieEndpoints(); +app.ConfigureScreeningEndpoints(); +app.ConfigureTicketEndpoints(); + +app.MapControllers(); + app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs new file mode 100644 index 00000000..45af234f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs @@ -0,0 +1,16 @@ +using System.Linq.Expressions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository +{ + public interface IRepository + { + Task> GetAll(); + Task GetById(int id); + Task Update(T entity); + Task Delete(int id); + Task Insert(T entity); + Task> GetWithIncludes(Func, IQueryable> includeQuery); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs new file mode 100644 index 00000000..821186a8 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs @@ -0,0 +1,60 @@ +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace api_cinema_challenge.Repository +{ + public class Repository : IRepository where T : class + { + private CinemaContext _database; + private DbSet _table = null!; + + public Repository(CinemaContext cinemaContext) + { + _database = cinemaContext; + _table = _database.Set(); + } + + public async Task Insert(T entity) + { + await _table.AddAsync(entity); + await _database.SaveChangesAsync(); + return entity; + } + + public async Task Delete(int id) + { + T entity = await _table.FindAsync(id); + _table.Remove(entity); + await _database.SaveChangesAsync(); + return entity; + } + + public async Task> GetAll() + { + return await _table.ToListAsync(); + } + + public async Task GetById(int id) + { + T entity = await _table.FindAsync(id); + return entity; + } + + public async Task Update(T entity) + { + _table.Attach(entity); + _database.Entry(entity).State = EntityState.Modified; + await _database.SaveChangesAsync(); + return entity; + } + + public async Task> GetWithIncludes(Func, IQueryable> includeQuery) + { + IQueryable query = includeQuery(_table); + return await query.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..088b30de --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs @@ -0,0 +1,84 @@ +using api_cinema_challenge.Models; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Http.HttpResults; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Identity; + +namespace api_cinema_challenge.Services +{ + public class TokenService + { + private const int ExpirationMinutes = 60; + private readonly ILogger _logger; + public TokenService(ILogger logger) + { + _logger = logger; + } + + public string CreateToken(ApplicationUser user) + { + + var expiration = DateTime.UtcNow.AddMinutes(ExpirationMinutes); + var token = CreateJwtToken( + CreateClaims(user), + CreateSigningCredentials(), + expiration + ); + var tokenHandler = new JwtSecurityTokenHandler(); + + _logger.LogInformation("JWT Token created"); + + return tokenHandler.WriteToken(token); + } + + private JwtSecurityToken CreateJwtToken(List claims, SigningCredentials credentials, + DateTime expiration) => + new( + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidIssuer"], + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidAudience"], + claims, + expires: expiration, + signingCredentials: credentials + ); + + private List CreateClaims(ApplicationUser user) + { + var jwtSub = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["JwtRegisteredClaimNamesSub"]; + + try + { + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, jwtSub), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()) + }; + + return claims; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + private SigningCredentials CreateSigningCredentials() + { + var symmetricSecurityKey = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["SymmetricSecurityKey"]; + + return new SigningCredentials( + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + SecurityAlgorithms.HmacSha256 + ); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj index 11e5c66b..82fd9806 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 @@ -15,8 +15,16 @@ + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -27,8 +35,4 @@ - - - -