Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using api_cinema_challenge.Dtos;
using api_cinema_challenge.Models;
using api_cinema_challenge.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace api_cinema_challenge.Controllers
{
[ApiController]
[Route("customers")]
public class CustomersController : ControllerBase
{
private readonly ICustomerRepository _repo;
public CustomersController(ICustomerRepository repo) => _repo = repo;

[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<CustomerDto>> Create(CreateCustomer req)
{
var c = await _repo.AddAsync(new Customer { Name = req.Name, Email = req.Email, Phone = req.Phone });
return CreatedAtAction(nameof(GetAll), null,
new CustomerDto(c.Id, c.Name, c.Email, c.Phone, c.CreatedAt, c.UpdatedAt));
}

[HttpGet]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<IEnumerable<CustomerDto>>> GetAll()
{
var list = await _repo.GetAllAsync();
return Ok(list.Select(c => new CustomerDto(c.Id, c.Name, c.Email, c.Phone, c.CreatedAt, c.UpdatedAt)));
}

[HttpPut("{id:int}")]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<CustomerDto>> Update(int id, CreateCustomer req)
{
var updated = await _repo.UpdateAsync(id, new Customer { Name = req.Name, Email = req.Email, Phone = req.Phone });
if (updated == null) return NotFound();
return Ok(new CustomerDto(updated.Id, updated.Name, updated.Email, updated.Phone, updated.CreatedAt, updated.UpdatedAt));
}

[HttpDelete("{id:int}")]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<CustomerDto>> Delete(int id)
{
var deleted = await _repo.DeleteAsync(id);
if (deleted == null) return NotFound();
return Ok(new CustomerDto(deleted.Id, deleted.Name, deleted.Email, deleted.Phone, deleted.CreatedAt, deleted.UpdatedAt));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using api_cinema_challenge.Dtos;
using api_cinema_challenge.Models;
using api_cinema_challenge.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace api_cinema_challenge.Controllers
{
[ApiController]
[Route("movies")]
public class MoviesController : ControllerBase
{
private readonly IMovieRepository _movies;
private readonly IScreeningRepository _screenings;

public MoviesController(IMovieRepository movies, IScreeningRepository screenings)
{ _movies = movies; _screenings = screenings; }

[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<MovieDto>> Create(CreateMovie req)
{
var m = await _movies.AddAsync(new Movie { Title = req.Title, Rating = req.Rating, Description = req.Description, RuntimeMins = req.RuntimeMins });
return CreatedAtAction(nameof(GetAll), null,
new MovieDto(m.Id, m.Title, m.Rating, m.Description, m.RuntimeMins, m.CreatedAt, m.UpdatedAt));
}

[HttpGet]
[Authorize]
public async Task<ActionResult<IEnumerable<MovieDto>>> GetAll()
{
var list = await _movies.GetAllAsync();
return Ok(list.Select(m => new MovieDto(m.Id, m.Title, m.Rating, m.Description, m.RuntimeMins, m.CreatedAt, m.UpdatedAt)));
}

[HttpPut("{id:int}")]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<MovieDto>> Update(int id, UpdateMovie req)
{
var updated = await _movies.UpdateAsync(id, new Movie { Title = req.Title, Rating = req.Rating, Description = req.Description, RuntimeMins = req.RuntimeMins });
if (updated == null) return NotFound();
return Ok(new MovieDto(updated.Id, updated.Title, updated.Rating, updated.Description, updated.RuntimeMins, updated.CreatedAt, updated.UpdatedAt));
}

[HttpDelete("{id:int}")]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<MovieDto>> Delete(int id)
{
var deleted = await _movies.DeleteAsync(id);
if (deleted == null) return NotFound();
return Ok(new MovieDto(deleted.Id, deleted.Title, deleted.Rating, deleted.Description, deleted.RuntimeMins, deleted.CreatedAt, deleted.UpdatedAt));
}

[HttpPost("{id:int}/screenings")]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<ScreeningDto>> CreateScreening(int id, CreateScreening req)
{
var startsUtc = req.StartsAt.Kind == DateTimeKind.Utc
? req.StartsAt
: DateTime.SpecifyKind(req.StartsAt, DateTimeKind.Utc);

var s = await _screenings.AddForMovieAsync(id, new Screening
{
ScreenNumber = req.ScreenNumber,
Capacity = req.Capacity,
StartsAt = startsUtc
});

return CreatedAtAction(nameof(GetScreenings), new { id },
new ScreeningDto(s.Id, s.ScreenNumber, s.Capacity, s.StartsAt, s.CreatedAt, s.UpdatedAt));
}

[HttpGet("{id:int}/screenings")]
[Authorize]
public async Task<ActionResult<IEnumerable<ScreeningDto>>> GetScreenings(int id)
{
var list = await _screenings.GetForMovieAsync(id);
return Ok(list.Select(s => new ScreeningDto(s.Id, s.ScreenNumber, s.Capacity, s.StartsAt, s.CreatedAt, s.UpdatedAt)));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using api_cinema_challenge.Data;
using api_cinema_challenge.DataTransfer.Requests;
using api_cinema_challenge.DataTransfer.Response;
using api_cinema_challenge.Enum;
using api_cinema_challenge.Models;
using api_cinema_challenge.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace api_cinema_challenge.Controllers
{

[ApiController]
[Route("/api/[controller]")]
public class UsersController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly CinemaContext _context;
private readonly TokenService _tokenService;

public UsersController(UserManager<ApplicationUser> userManager, CinemaContext context,
TokenService tokenService, ILogger<UsersController> logger)
{
_userManager = userManager;
_context = context;
_tokenService = tokenService;
}


[HttpPost]
[Route("register")]
public async Task<IActionResult> 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<ActionResult<AuthResponse>> 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.ApplicationUsers.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,
});
}
}
}
5 changes: 5 additions & 0 deletions api-cinema-challenge/api-cinema-challenge/DTO/CustomerDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace api_cinema_challenge.Dtos
{
public record CreateCustomer(string Name, string Email, string Phone);
public record CustomerDto(int Id, string Name, string Email, string Phone, DateTime CreatedAt, DateTime UpdatedAt);
}
6 changes: 6 additions & 0 deletions api-cinema-challenge/api-cinema-challenge/DTO/MovieDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace api_cinema_challenge.Dtos
{
public record CreateMovie(string Title, string Rating, string Description, int RuntimeMins);
public record UpdateMovie(string Title, string Rating, string Description, int RuntimeMins);
public record MovieDto(int Id, string Title, string Rating, string Description, int RuntimeMins, DateTime CreatedAt, DateTime UpdatedAt);
}
5 changes: 5 additions & 0 deletions api-cinema-challenge/api-cinema-challenge/DTO/ScreeningDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace api_cinema_challenge.Dtos
{
public record CreateScreening(int ScreenNumber, int Capacity, DateTime StartsAt);
public record ScreeningDto(int Id, int ScreenNumber, int Capacity, DateTime StartsAt, DateTime CreatedAt, DateTime UpdatedAt);
}
65 changes: 64 additions & 1 deletion api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using api_cinema_challenge.Models;

namespace api_cinema_challenge.Data
{
Expand All @@ -18,9 +18,72 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
optionsBuilder.UseNpgsql(_connectionString);
}

public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Movie> Movies => Set<Movie>();
public DbSet<Screening> Screenings => Set<Screening>();
public DbSet<Ticket> Tickets => Set<Ticket>();
public DbSet<ApplicationUser> ApplicationUsers { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// UTC timestamptz for tracked timestamps
modelBuilder.Entity<Customer>().Property(p => p.CreatedAt).HasColumnType("timestamptz");
modelBuilder.Entity<Customer>().Property(p => p.UpdatedAt).HasColumnType("timestamptz");
modelBuilder.Entity<Movie>().Property(p => p.CreatedAt).HasColumnType("timestamptz");
modelBuilder.Entity<Movie>().Property(p => p.UpdatedAt).HasColumnType("timestamptz");
modelBuilder.Entity<Screening>().Property(p => p.StartsAt).HasColumnType("timestamptz");
modelBuilder.Entity<Screening>().Property(p => p.CreatedAt).HasColumnType("timestamptz");
modelBuilder.Entity<Screening>().Property(p => p.UpdatedAt).HasColumnType("timestamptz");
modelBuilder.Entity<Ticket>().Property(p => p.CreatedAt).HasColumnType("timestamptz");
modelBuilder.Entity<Ticket>().Property(p => p.UpdatedAt).HasColumnType("timestamptz");

// Screening -> Movie
modelBuilder.Entity<Screening>()
.HasOne(s => s.Movie)
.WithMany(m => m.Screenings)
.HasForeignKey(s => s.MovieId)
.OnDelete(DeleteBehavior.Cascade);

// Ticket table + relations
modelBuilder.Entity<Ticket>().ToTable("ticket");
modelBuilder.Entity<Ticket>()
.HasOne(t => t.Customer)
.WithMany()
.HasForeignKey(t => t.CustomerId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Ticket>()
.HasOne(t => t.Screening)
.WithMany()
.HasForeignKey(t => t.ScreeningId)
.OnDelete(DeleteBehavior.Cascade);

// Required fields per spec
modelBuilder.Entity<Movie>().Property(m => m.Title).IsRequired();
modelBuilder.Entity<Movie>().Property(m => m.Rating).IsRequired();
modelBuilder.Entity<Movie>().Property(m => m.Description).IsRequired();
modelBuilder.Entity<Movie>().Property(m => m.RuntimeMins).IsRequired();

modelBuilder.Entity<Customer>().Property(c => c.Name).IsRequired();
modelBuilder.Entity<Customer>().Property(c => c.Email).IsRequired();
modelBuilder.Entity<Customer>().Property(c => c.Phone).IsRequired();
}

public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow;
foreach (var e in ChangeTracker.Entries())
{
if (e.State == EntityState.Added)
{
if (e.Metadata.FindProperty("CreatedAt") != null) e.CurrentValues["CreatedAt"] = now;
if (e.Metadata.FindProperty("UpdatedAt") != null) e.CurrentValues["UpdatedAt"] = now;
}
else if (e.State == EntityState.Modified)
{
if (e.Metadata.FindProperty("UpdatedAt") != null) e.CurrentValues["UpdatedAt"] = now;
}
}
return base.SaveChangesAsync(cancellationToken);
}
}
}
Loading