From a6262a681c9896d4e5bfe9dfbd418b9ff4763ba5 Mon Sep 17 00:00:00 2001 From: Tein Schoemaker Date: Sun, 24 Aug 2025 23:02:53 +0200 Subject: [PATCH 1/3] TODO: Security --- .gitignore | 2 + api-cinema-challenge/api-cinema-challenge.sln | 1 + .../api-cinema-challenge/Data/Cinema ERD.png | Bin 0 -> 22192 bytes .../api-cinema-challenge/Data/Seeder.cs | 7 ++ .../Requests/CustomerPostRequest.cs | 9 ++ .../DataTransfer/Requests/MoviePostRequest.cs | 15 +++ .../Requests/ScreeningPostRequest.cs | 12 ++ .../Requests/TicketPostRequest.cs | 7 ++ .../Endpoints/CustomerEndpoints.cs | 79 ++++++++++++++ .../Endpoints/MovieEndpoints.cs | 103 ++++++++++++++++++ .../Endpoints/ScreeningEndpoints.cs | 58 ++++++++++ .../Models/ApplicationUser.cs | 10 ++ .../api-cinema-challenge/Models/Customer.cs | 24 ++++ .../Models/Enums/MovieRating.cs | 11 ++ .../api-cinema-challenge/Models/Enums/Role.cs | 8 ++ .../api-cinema-challenge/Models/Movie.cs | 28 +++++ .../Models/ParentModel.cs | 12 ++ .../api-cinema-challenge/Models/Screening.cs | 27 +++++ .../api-cinema-challenge/Models/Ticket.cs | 26 +++++ .../api-cinema-challenge/Program.cs | 7 ++ .../Repository/IRepository.cs | 13 +++ .../Repository/Repository.cs | 47 ++++++++ .../api-cinema-challenge.csproj | 2 +- .../appsettings.example.json | 12 -- 24 files changed, 507 insertions(+), 13 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Data/Cinema ERD.png create mode 100644 api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/CustomerPostRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/MoviePostRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/ScreeningPostRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/TicketPostRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Customer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Enums/MovieRating.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Enums/Role.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Movie.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/ParentModel.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Screening.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs delete mode 100644 api-cinema-challenge/api-cinema-challenge/appsettings.example.json diff --git a/.gitignore b/.gitignore index cf332414..608ac5c0 100644 --- a/.gitignore +++ b/.gitignore @@ -362,6 +362,8 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd +Migrations/ + */**/appsettings.json */**/appsettings.Development.json */**/bin/Debug diff --git a/api-cinema-challenge/api-cinema-challenge.sln b/api-cinema-challenge/api-cinema-challenge.sln index 9cd490f5..c9e14d71 100644 --- a/api-cinema-challenge/api-cinema-challenge.sln +++ b/api-cinema-challenge/api-cinema-challenge.sln @@ -8,6 +8,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3C371BAA-344D-4C8A-AF08-7829816D726F}" ProjectSection(SolutionItems) = preProject ..\.gitignore = ..\.gitignore + api-cinema-challenge\Data\Cinema ERD.png = api-cinema-challenge\Data\Cinema ERD.png ..\README.md = ..\README.md EndProjectSection EndProject diff --git a/api-cinema-challenge/api-cinema-challenge/Data/Cinema ERD.png b/api-cinema-challenge/api-cinema-challenge/Data/Cinema ERD.png new file mode 100644 index 0000000000000000000000000000000000000000..b2d1703c3849ac876430221bde865e802daed2f1 GIT binary patch literal 22192 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 N%L}$<)uxwk{1Xw!VaWgh literal 0 HcmV?d00001 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..2e3b2dcb --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs @@ -0,0 +1,7 @@ +namespace api_cinema_challenge.Data +{ + public class Seeder + { + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/CustomerPostRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/CustomerPostRequest.cs new file mode 100644 index 00000000..f29f791e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/CustomerPostRequest.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DataTransfer.Requests +{ + public class CustomerPostRequest + { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/MoviePostRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/MoviePostRequest.cs new file mode 100644 index 00000000..699624af --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/MoviePostRequest.cs @@ -0,0 +1,15 @@ +using api_cinema_challenge.Models; +using api_cinema_challenge.Models.Enums; +using Npgsql.PostgresTypes; + +namespace api_cinema_challenge.DataTransfer.Requests +{ + public class MoviePostRequest + { + public string Title { get; set; } + public MovieRating Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + public List screenings { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/ScreeningPostRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/ScreeningPostRequest.cs new file mode 100644 index 00000000..168762be --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/ScreeningPostRequest.cs @@ -0,0 +1,12 @@ +using api_cinema_challenge.Models; +using Npgsql.PostgresTypes; + +namespace api_cinema_challenge.DataTransfer.Requests +{ + public class ScreenPostRequest + { + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/TicketPostRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/TicketPostRequest.cs new file mode 100644 index 00000000..50df326c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/TicketPostRequest.cs @@ -0,0 +1,7 @@ +namespace api_cinema_challenge.DataTransfer.Requests +{ + public class TicketPostRequest + { + public int NumSeats { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs new file mode 100644 index 00000000..5b0bbead --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs @@ -0,0 +1,79 @@ +using api_cinema_challenge.DataTransfer.Requests; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class CustomerEndpoints + { + public static void ConfigureCustomerEndpoint(this WebApplication app) + { + var customers = app.MapGroup("/customers"); + + customers.MapPost("/", Create); + customers.MapGet("/", GetAll); + customers.MapPut("/{id}", Update); + customers.MapDelete("/{id}", Delete); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task Create(IRepository repository, CustomerPostRequest customer) + { + var newCustomer = new Customer() + { + Name = customer.Name, + Email = customer.Email, + Phone = customer.Phone, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await repository.Create(newCustomer); + return TypedResults.Created($"/{newCustomer.Id}", newCustomer); + } + + [Authorize(Roles = "User")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository repository) + { + var entities = await repository.GetAll(); + List results = new List(); + + foreach (var entity in entities) + { + results.Add(new { Name = entity.Name, Email = entity.Email, Phone = entity.Phone }); + } + + return TypedResults.Ok(results); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task Update(IRepository repository, int id, CustomerPostRequest customer) + { + var entity = await repository.GetById(id); + + entity.Name = !string.IsNullOrEmpty(customer.Name) ? customer.Name : entity.Name; + entity.Phone = !string.IsNullOrEmpty(customer.Phone) ? customer.Phone : entity.Phone; + entity.Email = !string.IsNullOrEmpty(customer.Email) ? customer.Email : entity.Email; + entity.UpdatedAt = DateTime.UtcNow; + + var result = await repository.Update(entity); + + return result != null ? TypedResults.Ok(new { Name = result.Name, Phone = result.Phone, Email = result.Email ,UpdatedAt = result.UpdatedAt}) : TypedResults.BadRequest("Couldn't save to the database?!"); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task Delete(IRepository repository, int id) + { + var entity = await repository.GetById(id); + var result = await repository.Delete(entity); + + return result != null ? TypedResults.Ok(new { Name = result.Name, Phone = result.Phone, Email = result.Email, UpdatedAt = result.UpdatedAt }) : TypedResults.BadRequest("Object wasnt deleted?!"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs new file mode 100644 index 00000000..e73b30be --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs @@ -0,0 +1,103 @@ +using api_cinema_challenge.DataTransfer.Requests; +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 MovieEndpoints + { + public static void ConfigureMovieEndpoint(this WebApplication app) + { + var movies = app.MapGroup("/movies"); + + movies.MapPost("/", Create); + movies.MapGet("/", GetAll); + movies.MapPut("/{id}", Update); + movies.MapDelete("/{id}", Delete); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task Create(IRepository repository, MoviePostRequest movie) + { + var newMovie = new Movie() + { + Title = movie.Title, + Rating = movie.Rating, + Description = movie.Description, + RuntimeMins = movie.RuntimeMins, + Screenings = movie.screenings, + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now + }; + + await repository.Create(newMovie); + return TypedResults.Created($"/{newMovie.Id}", newMovie); + } + + [Authorize(Roles = "User")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository repository) + { + var entities = await repository.GetAll(); + List results = new List(); + + foreach (var entity in entities) + { + results.Add(new { + Title = entity.Title, + Rating = entity.Rating, + Description = entity.Description, + RuntimeMins = entity.RuntimeMins, + Screenings = entity.Screenings + }); + } + + return TypedResults.Ok(results); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task Update(IRepository repository, int id, MoviePostRequest movie) + { + var entity = await repository.GetById(id); + + entity.Title = !string.IsNullOrEmpty(movie.Title) ? movie.Title : entity.Title; + entity.Rating = movie.Rating; + entity.Description = !string.IsNullOrEmpty(movie.Description) ? movie.Description : entity.Description; + entity.RuntimeMins = movie.RuntimeMins; + entity.Screenings = movie.screenings; + entity.UpdatedAt = DateTime.UtcNow; + + var result = await repository.Update(entity); + + return result != null ? TypedResults.Ok(new { + Title = entity.Title, + Rating = entity.Rating, + Description = entity.Description, + RuntimeMins = entity.RuntimeMins, + Screenings = entity.Screenings, + UpdatedAt = result.UpdatedAt + }) : TypedResults.BadRequest("Couldn't save to the database?!"); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task Delete(IRepository repository, int id) + { + var entity = await repository.GetById(id); + var result = await repository.Delete(entity); + + return result != null ? TypedResults.Ok(new { + Title = entity.Title, + Rating = entity.Rating, + Description = entity.Description, + RuntimeMins = entity.RuntimeMins, + Screenings = entity.Screenings, + UpdatedAt = result.UpdatedAt + }) : TypedResults.BadRequest("Object wasnt deleted?!"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs new file mode 100644 index 00000000..fecfe0b9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs @@ -0,0 +1,58 @@ +using api_cinema_challenge.DataTransfer.Requests; +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 ScreeningEndpoints + { + public static void ConfigureScreeningEndpoint(this WebApplication app) + { + var screenings = app.MapGroup("/screenings"); + + screenings.MapPost("/", Create); + screenings.MapGet("/", GetAll); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task Create(IRepository repository, ScreenPostRequest screening, int movieId) + { + var newScreening = new Screening() + { + ScreenNumber = screening.ScreenNumber, + Capacity = screening.Capacity, + startsAt = screening.StartsAt, + MovieId = movieId, + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now + }; + + await repository.Create(newScreening); + return TypedResults.Created($"/{newScreening.Id}", newScreening); + } + + [Authorize(Roles = "User")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository repository) + { + var entities = await repository.GetAll(); + List results = new List(); + + foreach (var entity in entities) + { + results.Add(new + { + ScreenNumber = entity.ScreenNumber, + Capacity = entity.Capacity, + StartsAt = entity.startsAt, + MovieId = entity.MovieId + }); + } + + return TypedResults.Ok(results); + } + } +} 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..3dbf15b5 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/ApplicationUser.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity; +using api_cinema_challenge.Models.Enums; + +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..b17d95e4 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("customers")] + public class Customer : ParentModel + { + [Key] + [Column("id")] + 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; } + + public List Tickets { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Enums/MovieRating.cs b/api-cinema-challenge/api-cinema-challenge/Models/Enums/MovieRating.cs new file mode 100644 index 00000000..008ea3ba --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Enums/MovieRating.cs @@ -0,0 +1,11 @@ +namespace api_cinema_challenge.Models.Enums +{ + public enum MovieRating + { + G, + PG, + PG13, + R, + NC17 + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Enums/Role.cs b/api-cinema-challenge/api-cinema-challenge/Models/Enums/Role.cs new file mode 100644 index 00000000..45bfa19e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Enums/Role.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.Models.Enums +{ + public enum Role + { + Admin, + User + } +} 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..089c0fa0 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,28 @@ +using api_cinema_challenge.Models.Enums; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("movies")] + public class Movie : ParentModel + { + [Key] + [Column("id")] + public int Id { get; set; } + + [Column("title")] + public string Title { get; set; } + + [Column("rating")] + public MovieRating Rating { get; set; } + + [Column("description")] + public string Description { get; set; } + + [Column("runtimemins")] + public int RuntimeMins { get; set; } + + public List Screenings { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/ParentModel.cs b/api-cinema-challenge/api-cinema-challenge/Models/ParentModel.cs new file mode 100644 index 00000000..cf675a70 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/ParentModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + public class ParentModel + { + [Column("createdat")] + public DateTime CreatedAt { get; set; } + [Column("updatedat")] + 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..9cae32bc --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("screenings")] + public class Screening : ParentModel + { + [Key] + [Column("id")] + public int Id { get; set; } + + [ForeignKey("movies")] + public int MovieId { get; set; } + + [Column("screennumber")] + public int ScreenNumber { get; set; } + + [Column("capacity")] + public int Capacity { get; set; } + + [Column("startsat")] + public DateTime startsAt { get; set; } + + public List Tickets { 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..de1fe745 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; + +namespace api_cinema_challenge.Models +{ + [Table("tickets")] + public class Ticket : ParentModel + { + [Key] + [Column("id")] + public int Id { get; set; } + + [Column("numseats")] + public int NumSeats { get; set; } + + [ForeignKey("screenings")] + public int ScreeningId { get; set; } + + [ForeignKey("customers")] + public int CustomerId { get; set; } + + public Screening Screening { get; set; } + public Customer Customer { get; set; } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..7ad45453 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,4 +1,7 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Repository; +using api_cinema_challenge.Models; var builder = WebApplication.CreateBuilder(args); @@ -6,6 +9,7 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext(); +builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); var app = builder.Build(); @@ -17,4 +21,7 @@ } app.UseHttpsRedirection(); +app.ConfigureCustomerEndpoint(); +app.ConfigureMovieEndpoint(); +app.ConfigureScreeningEndpoint(); 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..d0b3ec03 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs @@ -0,0 +1,13 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository +{ + public interface IRepository + { + Task Create(T entity); + Task GetById(object id); + Task> GetAll(); + Task Update(T entity); + Task Delete(T entity); + } +} 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..2d2957cd --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs @@ -0,0 +1,47 @@ +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository +{ + public class Repository : IRepository where T : class + { + private CinemaContext _db; + private DbSet _table = null; + public Repository(CinemaContext db) + { + _db = db; + _table = _db.Set(); + } + + public async Task Create(T entity) + { + _table.Add(entity); + await _db.SaveChangesAsync(); + return entity; + } + + public async Task GetById(object id) + { + return await _table.FindAsync(id); + } + + public async Task> GetAll() + { + return await _table.ToListAsync(); + } + + public async Task Update(T entity) + { + _table.Update(entity).State = EntityState.Modified; + await _db.SaveChangesAsync(); + return entity; + } + + public async Task Delete(T entity) + { + _db.Entry(entity).State = EntityState.Deleted; + await _db.SaveChangesAsync(); + 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..fa8193a4 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -28,7 +28,7 @@ - + diff --git a/api-cinema-challenge/api-cinema-challenge/appsettings.example.json b/api-cinema-challenge/api-cinema-challenge/appsettings.example.json deleted file mode 100644 index b9175fe6..00000000 --- a/api-cinema-challenge/api-cinema-challenge/appsettings.example.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "ConnectionStrings": { - "DefaultConnectionString": "Host=HOST; Database=DATABASE; Username=USERNAME; Password=PASSWORD;" - } -} \ No newline at end of file From b56bd39d2c32bab2aaf993d475211f45408c8f4f Mon Sep 17 00:00:00 2001 From: Tein Schoemaker Date: Mon, 25 Aug 2025 15:32:03 +0200 Subject: [PATCH 2/3] Core + Extension --- .../Data/CinemaContext.cs | 12 ++ .../api-cinema-challenge/Data/Seeder.cs | 140 +++++++++++++++++- .../Endpoints/TicketEndpoints.cs | 56 +++++++ .../api-cinema-challenge/Program.cs | 1 + 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoints.cs diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index ad4fe854..70a9151d 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; +using api_cinema_challenge.Models; namespace api_cinema_challenge.Data { @@ -20,7 +21,18 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { + Seeder seeder = new Seeder(); + modelBuilder.Entity().HasData(seeder.Customers); + modelBuilder.Entity().HasData(seeder.Movies); + modelBuilder.Entity().HasData(seeder.Screenings); + modelBuilder.Entity().HasData(seeder.Tickets); + base.OnModelCreating(modelBuilder); } + public DbSet Customers { get; set; } + public DbSet Movies { get; set; } + public DbSet Screenings { get; set; } + public DbSet Tickets { get; set; } + } } diff --git a/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs index 2e3b2dcb..857b425a 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs @@ -1,7 +1,145 @@ -namespace api_cinema_challenge.Data +using api_cinema_challenge.Models; +using api_cinema_challenge.Models.Enums; + +namespace api_cinema_challenge.Data { public class Seeder { + private List _firstNames = new List() + { + "Audrey", + "Donald", + "Elvis", + "Barack", + "Oprah", + "Jimi", + "Mick", + "Kate", + "Charles", + "Kate" + }; + + private List _lastNames = new List() + { + "Hepburn", + "Trump", + "Presley", + "Obama", + "Winfrey", + "Hendrix", + "Jagger", + "Winslet", + "Windsor", + "Middleton" + }; + + private List _domains = new List() + { + "gmail.com", + "google.com", + "hotmail.com", + "something.com", + "mcdonalds.com", + "nasa.org.us", + "gov.us", + "gov.gr", + "gov.nl", + "gov.ru" + }; + + private List _movieTitles = new List() + { + "The Lost Kingdom", "Space Odyssey", "Dreamcatcher", + "Ocean Deep", "Hidden Truths", "Shadows Rising", + "Eternal Flame", "The Great Escape", "Parallel Worlds", "Infinite Loop" + }; + + private List _descriptions = new List() + { + "An epic adventure across unknown lands.", + "A thrilling journey through space and time.", + "A heartwarming story of friendship and courage.", + "A suspenseful drama filled with mystery.", + "A hilarious comedy for the whole family.", + "A dark tale of betrayal and survival." + }; + + private List _customers = new List(); + private List _movies = new List(); + private List _screenings = new List(); + private List _tickets = new List(); + + public Seeder() + { + Random random = new Random(); + + for (int x = 1; x < 50; x++) + { + var first = _firstNames[random.Next(_firstNames.Count)]; + var last = _lastNames[random.Next(_lastNames.Count)]; + var domain = _domains[random.Next(_domains.Count)]; + + Customer customer = new Customer + { + Id = x, + Name = $"{first} {last}", + Email = $"{first}{last}@{domain}", + Phone = $"06{random.Next(1000, 9999)}{random.Next(1000, 9999)}" + }; + _customers.Add(customer); + } + + for (int y = 1; y < 50; y++) + { + Movie movie = new Movie + { + Id = y, + Title = _movieTitles[random.Next(_movieTitles.Count)], + Rating = (MovieRating)random.Next(Enum.GetNames(typeof(MovieRating)).Length), + Description = _descriptions[random.Next(_descriptions.Count)], + RuntimeMins = random.Next(90, 180) + }; + _movies.Add(movie); + } + + foreach (var movie in _movies) + { + int screeningsAmount = random.Next(1, 5); + for (int z = 0; z < screeningsAmount; z++) + { + Screening screening = new Screening + { + Id = z, + MovieId = movie.Id, + ScreenNumber = random.Next(1, 5), + Capacity = random.Next(20, 60), + startsAt = DateTime.UtcNow.AddDays(random.Next(1, 15)) + }; + } + } + + foreach (var screening in _screenings) + { + int ticketsAmount = random.Next(1, 60); + for (int a = 0; a < ticketsAmount; a++) + { + var customer = _customers[random.Next(_customers.Count)]; + + Ticket ticket = new Ticket + { + Id = a, + NumSeats = random.Next(1, 5), + ScreeningId = screening.Id, + CustomerId = customer.Id + }; + _tickets.Add(ticket); + } + } + } + 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/TicketEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoints.cs new file mode 100644 index 00000000..278898c9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoints.cs @@ -0,0 +1,56 @@ +using api_cinema_challenge.DataTransfer.Requests; +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 TicketEndpoints + { + public static void ConfigureTicketEndpoints(this WebApplication app) + { + var screenings = app.MapGroup("/tickets"); + + screenings.MapPost("/", Create); + screenings.MapGet("/", GetAll); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task Create(IRepository repository, TicketPostRequest ticket, int customerId, int screeningId) + { + var newTicket = new Ticket() + { + NumSeats = ticket.NumSeats, + CustomerId = customerId, + ScreeningId = screeningId, + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now + }; + + await repository.Create(newTicket); + return TypedResults.Created($"/{newTicket.Id}", newTicket); + } + + [Authorize(Roles = "User")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository repository) + { + var entities = await repository.GetAll(); + List results = new List(); + + foreach (var entity in entities) + { + results.Add(new + { + NumSeats = entity.NumSeats, + CustomerId = entity.CustomerId, + ScreeningId = entity.ScreeningId, + }); + } + + return TypedResults.Ok(results); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 7ad45453..2cd9dc31 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -24,4 +24,5 @@ app.ConfigureCustomerEndpoint(); app.ConfigureMovieEndpoint(); app.ConfigureScreeningEndpoint(); +app.ConfigureTicketEndpoints(); app.Run(); From 0a894ff0a5262ab393a32a3d49747e849c3fc823 Mon Sep 17 00:00:00 2001 From: Tein Schoemaker Date: Mon, 25 Aug 2025 16:32:07 +0200 Subject: [PATCH 3/3] Oops, JWS --- .../Controllers/UserController.cs | 100 ++++++++++++++++++ .../Data/CinemaContext.cs | 4 +- .../DataTransfer/Requests/AuthRequest.cs | 13 +++ .../Requests/RegistrationRequest.cs | 19 ++++ .../DataTransfer/Response/AuthResponse.cs | 9 ++ .../Helpers/ClaimsPrincipalHelpers.cs | 28 +++++ .../api-cinema-challenge/Program.cs | 77 +++++++++++++- .../Services/TokenService.cs | 82 ++++++++++++++ .../api-cinema-challenge.csproj | 10 +- 9 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelpers.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs 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..19fce7f7 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Controllers/UserController.cs @@ -0,0 +1,100 @@ +using api_cinema_challenge.Models; +using api_cinema_challenge.Models.Enums; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using api_cinema_challenge.Data; +using api_cinema_challenge.DataTransfer.Requests; +using api_cinema_challenge.DataTransfer.Response; +using api_cinema_challenge.Services; + +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/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index 70a9151d..946f001e 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,10 +1,12 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; using api_cinema_challenge.Models; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; namespace api_cinema_challenge.Data { - public class CinemaContext : DbContext + public class CinemaContext : IdentityUserContext { private string _connectionString; public CinemaContext(DbContextOptions options) : base(options) diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs new file mode 100644 index 00000000..606db053 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/AuthRequest.cs @@ -0,0 +1,13 @@ +namespace api_cinema_challenge.DataTransfer.Requests +{ + public class AuthRequest + { + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() + { + return true; + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs new file mode 100644 index 00000000..3a26f463 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Requests/RegistrationRequest.cs @@ -0,0 +1,19 @@ +using api_cinema_challenge.Models.Enums; +using System.ComponentModel.DataAnnotations; + +namespace api_cinema_challenge.DataTransfer.Requests +{ + public class RegistrationRequest + { + [Required] + public string? Email { get; set; } + + [Required] + public string? Username { get { return this.Email; } set { } } + + [Required] + public string? Password { get; set; } + + public Role Role { get; set; } = Role.User; + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs new file mode 100644 index 00000000..b334a5a6 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataTransfer/Response/AuthResponse.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DataTransfer.Response +{ + public class AuthResponse + { + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelpers.cs b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelpers.cs new file mode 100644 index 00000000..3d4c0468 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Helpers/ClaimsPrincipalHelpers.cs @@ -0,0 +1,28 @@ +using System.Security.Claims; + +namespace api_cinema_challenge.Helpers +{ + public static class ClaimsPrincipalHelper + { + public static string? UserId(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.NameIdentifier); + return claim?.Value; + } + public static string? Email(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.Email); + return claim?.Value; + } + + // public static string? UserId(this IIdentity identity) + // { + // if (identity != null && identity.IsAuthenticated) + // { + // // return Guid.Parse(((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value); + // return ((ClaimsIdentity)identity).Claims.Where(x => x.Type == "NameIdentifier").FirstOrDefault()!.Value; + // } + // return null; + // } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index 2cd9dc31..ed0265a3 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,15 +1,81 @@ using api_cinema_challenge.Data; using api_cinema_challenge.Endpoints; -using api_cinema_challenge.Repository; using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using System.Text; 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.AddRouting(options => options.LowercaseUrls = true); builder.Services.AddDbContext(); builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); +builder.Services.AddScoped(); + +// 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(); @@ -21,8 +87,15 @@ } app.UseHttpsRedirection(); +app.UseStatusCodePages(); + +app.UseAuthentication(); +app.UseAuthorization(); + app.ConfigureCustomerEndpoint(); app.ConfigureMovieEndpoint(); app.ConfigureScreeningEndpoint(); app.ConfigureTicketEndpoints(); + +app.MapControllers(); app.Run(); 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..f10051e9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs @@ -0,0 +1,82 @@ +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; + +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 + ); + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj index fa8193a4..121cfb8c 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -8,13 +8,8 @@ - - - - - - - + + @@ -25,6 +20,7 @@ +