diff --git a/api-cinema-challenge/api-cinema-challenge/Controllers/UsersController.cs b/api-cinema-challenge/api-cinema-challenge/Controllers/UsersController.cs new file mode 100644 index 00000000..c6d2e096 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Controllers/UsersController.cs @@ -0,0 +1,102 @@ +using api_cinema_challenge.Data; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Data; +using api_cinema_challenge.Data; +using api_cinema_challenge.Enums; +using api_cinema_challenge.Services; +using api_cinema_challenge.DataModels; +using api_cinema_challenge.DTOs.Auth; + + +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, + }); + } + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthRequest.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthRequest.cs new file mode 100644 index 00000000..babf7d03 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthRequest.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.DTOs.Auth; + +public class AuthRequest +{ + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() + { + return true; + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthResponse.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthResponse.cs new file mode 100644 index 00000000..485165c2 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/AuthResponse.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs.Auth; + + +public class AuthResponse +{ + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/RegistrationRequest.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/RegistrationRequest.cs new file mode 100644 index 00000000..28db7938 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Auth/RegistrationRequest.cs @@ -0,0 +1,20 @@ + + +using System.ComponentModel.DataAnnotations; +using api_cinema_challenge.Enums; + +namespace api_cinema_challenge.DTOs.Auth; + +public class RegistrationRequest +{ + [Required] + public string? Email { get; set; } + + [Required] + public string? Username { get { return this.Email; } set { } } + + [Required] + public string? Password { get; set; } + + public Role Role { get; set; } = Role.User; +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Customers/CustomerPostPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Customers/CustomerPostPut.cs new file mode 100644 index 00000000..29c2179c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Customers/CustomerPostPut.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs.Customers +{ + public class CustomerPostPut + { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MoviePostPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MoviePostPut.cs new file mode 100644 index 00000000..c977a614 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Movies/MoviePostPut.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs.Movies +{ + public class MoviePostPut + { + 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/Screenings/ScreeningPostPut.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/Screenings/ScreeningPostPut.cs new file mode 100644 index 00000000..5ddf39cf --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/Screenings/ScreeningPostPut.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs.Screenings +{ + public class ScreeningPostPut + { + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index ad4fe854..cf7af413 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,9 +1,12 @@ -using Microsoft.EntityFrameworkCore; +using api_cinema_challenge.DataModels; +using api_cinema_challenge.Models; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; namespace api_cinema_challenge.Data { - public class CinemaContext : DbContext + public class CinemaContext : IdentityUserContext { private string _connectionString; public CinemaContext(DbContextOptions options) : base(options) @@ -20,7 +23,11 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { - + modelBuilder.Entity().HasData(new Movie { Id = 1, Description = "t", Rating = "1", Title = "bullshit", CreatedAt = new DateTime(2025, 8, 21, 13, 30, 0, DateTimeKind.Utc), UpdatedAt = new DateTime(2025, 8, 21, 13, 30, 0, DateTimeKind.Utc), RuntimeMins = 10 }); + base.OnModelCreating(modelBuilder); } + public DbSet Movies { get; set; } + public DbSet Screenings { get; set; } + public DbSet Customers { get; set; } } } diff --git a/api-cinema-challenge/api-cinema-challenge/DataModels/ApplicationUser.cs b/api-cinema-challenge/api-cinema-challenge/DataModels/ApplicationUser.cs new file mode 100644 index 00000000..78897485 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DataModels/ApplicationUser.cs @@ -0,0 +1,9 @@ +using api_cinema_challenge.Enums; +using Microsoft.AspNetCore.Identity; + + +namespace api_cinema_challenge.DataModels; +public class ApplicationUser : IdentityUser +{ + public Role Role { get; set; } +} \ No newline at end of file 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..01e7b6b8 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -0,0 +1,81 @@ +using api_cinema_challenge.DTOs.Customers; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; + +namespace api_cinema_challenge.Endpoints +{ + public static class CustomerEndpoint + { + public static void ConfigureCustomerEndpoints(this WebApplication app) + { + var customer = app.MapGroup("/customers"); + + customer.MapPost("/", Create); + customer.MapGet("/", GetAll); + customer.MapPut("/{id}", UpdateCustomer); + customer.MapDelete("/{id}", Delete); + + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository customerRepository) + { + + var results = await customerRepository.Get(); + + return TypedResults.Ok(results); + + } + + [Authorize(Roles = "User")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task Create(IRepository customerRepository, CustomerPostPut model) + { + Customer customer = new Customer() + { + Name = model.Name, + Email = model.Email, + Phone = model.Phone, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + var entity = await customerRepository.Insert(customer); + + + return TypedResults.Created($"", entity); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public static async Task UpdateCustomer(IRepository customerRepository, int id, CustomerPostPut model) + { + var resultGet = await customerRepository.GetOne(id); + resultGet.Name = model.Name; + resultGet.Email = model.Email; + resultGet.Phone = model.Phone; + resultGet.UpdatedAt = DateTime.UtcNow; + + var result = await customerRepository.Update(resultGet); + return TypedResults.Ok(result); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task Delete(IRepository customerRepository, int id) + { + var result = await customerRepository.Delete(id); + return TypedResults.Ok(result); + } + } +} 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..3fc24960 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -0,0 +1,80 @@ +using api_cinema_challenge.DTOs.Customers; +using api_cinema_challenge.DTOs.Movies; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; + +namespace api_cinema_challenge.Endpoints +{ + public static class MovieEndpoint + { + public static void ConfigureMovieEndpoints(this WebApplication app) + { + var movie = app.MapGroup("/movies"); + + movie.MapPost("/", Create); + movie.MapGet("/", GetAll); + movie.MapPut("/{id}", UpdateMovie); + movie.MapDelete("/{id}", Delete); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository movieRepository) + { + var results = await movieRepository.Get(); + + return TypedResults.Ok(results); + } + + [Authorize(Roles = "User")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task Create(IRepository movieRepository, MoviePostPut model) + { + Movie movie = new Movie() + { + Title = model.Title, + Rating = model.Rating, + Description = model.Description, + RuntimeMins = model.RuntimeMins, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + var entity = await movieRepository.Insert(movie); + + + return TypedResults.Created($"", entity); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public static async Task UpdateMovie(IRepository movieRepository, int id, MoviePostPut model) + { + var resultGet = await movieRepository.GetOne(id); + resultGet.Title = model.Title; + resultGet.Rating = model.Rating; + resultGet.Description = model.Description; + resultGet.RuntimeMins = model.RuntimeMins; + resultGet.UpdatedAt = DateTime.UtcNow; + + var result = await movieRepository.Update(resultGet); + return TypedResults.Ok(result); + } + + [Authorize(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task Delete(IRepository movieRepository, int id) + { + var result = await movieRepository.Delete(id); + return TypedResults.Ok(result); + } + } +} 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..8e7e84e4 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoint.cs @@ -0,0 +1,62 @@ +using api_cinema_challenge.DTOs.Movies; +using api_cinema_challenge.DTOs.Screenings; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; + +namespace api_cinema_challenge.Endpoints +{ + public static class ScreeningEndpoint + { + public static void ConfigureScreeningEndpoints(this WebApplication app) + { + var screening = app.MapGroup("/movies"); + + screening.MapPost("/{id}/screenings", Create); + screening.MapGet("/{id}/screenings", GetAll); + + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAll(IRepository screeningRepository, int id) + { + List results = (List)await screeningRepository.Get(); + List response = new List(); + foreach (var item in results) + { + if (item.movieId == id) + { + response.Add(item); + } + } + + return TypedResults.Ok(response); + } + + [Authorize(Roles = "User")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public static async Task Create(int id, IRepository screeningRepository, ScreeningPostPut model) + { + Screening screening = new Screening() + { + movieId = id, + ScreenNumber = model.ScreenNumber, + Capacity = model.Capacity, + StartsAt = model.StartsAt, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + var entity = await screeningRepository.Insert(screening); + + + return TypedResults.Created($"", entity); + } + + + } +} 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/Migrations/20250827090409_First.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250827090409_First.Designer.cs new file mode 100644 index 00000000..b233ccca --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250827090409_First.Designer.cs @@ -0,0 +1,298 @@ +// +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("20250827090409_First")] + partial class First + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("api_cinema_challenge.DataModels.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 21, 13, 30, 0, 0, DateTimeKind.Utc), + Description = "t", + Rating = "1", + RuntimeMins = 10, + Title = "bullshit", + UpdatedAt = new DateTime(2025, 8, 21, 13, 30, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("movieId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("api_cinema_challenge.DataModels.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("api_cinema_challenge.DataModels.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("api_cinema_challenge.DataModels.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250827090409_First.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250827090409_First.cs new file mode 100644 index 00000000..c6e8ed45 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250827090409_First.cs @@ -0,0 +1,207 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class First : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Role = table.Column(type: "integer", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Movies", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "text", nullable: false), + Rating = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + RuntimeMins = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Movies", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Screenings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + movieId = table.Column(type: "integer", nullable: false), + ScreenNumber = table.Column(type: "integer", nullable: false), + 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) + }, + constraints: table => + { + table.PrimaryKey("PK_Screenings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Movies", + columns: new[] { "Id", "CreatedAt", "Description", "Rating", "RuntimeMins", "Title", "UpdatedAt" }, + values: new object[] { 1, new DateTime(2025, 8, 21, 13, 30, 0, 0, DateTimeKind.Utc), "t", "1", 10, "bullshit", new DateTime(2025, 8, 21, 13, 30, 0, 0, DateTimeKind.Utc) }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Movies"); + + migrationBuilder.DropTable( + name: "Screenings"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs new file mode 100644 index 00000000..9b79fdf7 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,295 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + partial class CinemaContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("api_cinema_challenge.DataModels.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text"); + + b.Property("RuntimeMins") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + + b.HasData( + new + { + Id = 1, + CreatedAt = new DateTime(2025, 8, 21, 13, 30, 0, 0, DateTimeKind.Utc), + Description = "t", + Rating = "1", + RuntimeMins = 10, + Title = "bullshit", + UpdatedAt = new DateTime(2025, 8, 21, 13, 30, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ScreenNumber") + .HasColumnType("integer"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("movieId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("api_cinema_challenge.DataModels.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("api_cinema_challenge.DataModels.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("api_cinema_challenge.DataModels.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} 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..edcdff4e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.Models +{ + public class Customer + { + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs new file mode 100644 index 00000000..34970903 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,13 @@ +namespace api_cinema_challenge.Models +{ + public class Movie + { + public int Id { get; set; } + public string Title { get; set; } + public string Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs new file mode 100644 index 00000000..28cf2568 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,13 @@ +namespace api_cinema_challenge.Models +{ + public class Screening + { + public int Id { get; set; } + public int movieId { get; set; } + public int ScreenNumber { get; set; } + public 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/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..16dde6ec 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,11 +1,122 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.DataModels; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; +using System.Diagnostics; +using System.Text.Json.Serialization; +using System.Text; var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -builder.Services.AddDbContext(); + +builder.Services.AddProblemDetails(); +builder.Services.AddApiVersioning(); +builder.Services.AddRouting(options => options.LowercaseUrls = true); + +builder.Services.AddScoped(); + +builder.Services.AddDbContext(opt => +{ + opt.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); + opt.LogTo(message => Debug.WriteLine(message)); + +}); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); + +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[] { } + } + }); +}); + +// Support string to enum conversions +builder.Services.AddControllers().AddJsonOptions(opt => +{ + opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); + +// Specify identity requirements +// Must be added before .AddAuthentication otherwise a 404 is thrown on authorized endpoints +builder.Services + .AddIdentity(options => + { + options.SignIn.RequireConfirmedAccount = false; + options.User.RequireUniqueEmail = true; + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + }) + .AddRoles() + .AddEntityFrameworkStores(); + + +// These will eventually be moved to a secrets file, but for alpha development appsettings is fine +var validIssuer = builder.Configuration.GetValue("JwtTokenSettings:ValidIssuer"); +var validAudience = builder.Configuration.GetValue("JwtTokenSettings:ValidAudience"); +var symmetricSecurityKey = builder.Configuration.GetValue("JwtTokenSettings:SymmetricSecurityKey"); + + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +}) + .AddJwtBearer(options => + { + options.IncludeErrorDetails = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ClockSkew = TimeSpan.Zero, + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = validIssuer, + ValidAudience = validAudience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + }; + }); + + + var app = builder.Build(); @@ -17,4 +128,14 @@ } app.UseHttpsRedirection(); +app.UseStatusCodePages(); + +app.ConfigureCustomerEndpoints(); +app.ConfigureMovieEndpoints(); +app.ConfigureScreeningEndpoints(); + +app.UseAuthentication(); +app.UseAuthorization(); + +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..d30c1afd --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IRepository.cs @@ -0,0 +1,13 @@ +using System.Linq.Expressions; + +namespace api_cinema_challenge.Repository +{ + public interface IRepository + { + Task> Get(); + Task GetOne(int id); + Task Insert(T entity); + Task Update(T entity); + Task Delete(object id); + } +} 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..4795a726 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/Repository.cs @@ -0,0 +1,51 @@ +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; +using System.Net.Http.Headers; +using System.Runtime.Serialization; + + +namespace api_cinema_challenge.Repository +{ + public class Repository : IRepository where T : class + { + private CinemaContext _context; + private DbSet _table = null; + + public Repository(CinemaContext context) + { + _context = context; + _table = _context.Set(); + } + + public async Task> Get() + { + return await _table.ToListAsync(); + } + public async Task GetOne(int id) + { + return await _table.FindAsync(id); + } + public async Task Insert(T entity) + { + await _table.AddAsync(entity); + await _context.SaveChangesAsync(); + return entity; + } + public async Task Update(T entity) + { + _table.Update(entity).State = EntityState.Modified; + await _context.SaveChangesAsync(); + return entity; + } + + public async Task Delete(object id) + { + T entity = await _table.FindAsync(id); + _table.Remove(entity); + await _context.SaveChangesAsync(); + return entity; + } + + } +} 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..f8f9d3df --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Services/TokenService.cs @@ -0,0 +1,86 @@ +using Microsoft.AspNetCore.Http.HttpResults; + +namespace api_cinema_challenge.Services; + +using api_cinema_challenge.DataModels; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using api_cinema_challenge.DataModels; + + +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..728b3d5b 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -8,15 +8,16 @@ - - - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -27,8 +28,4 @@ - - - -