From 0f1d04dc0e368eb02fe1f6ff2afc8e9fc52877b8 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Thu, 6 Oct 2022 22:03:39 -0700 Subject: [PATCH 01/24] Get events --- TescEvents/Controllers/EventsController.cs | 5 +++-- TescEvents/Properties/launchSettings.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/TescEvents/Controllers/EventsController.cs b/TescEvents/Controllers/EventsController.cs index 4f80088..f38b6b0 100644 --- a/TescEvents/Controllers/EventsController.cs +++ b/TescEvents/Controllers/EventsController.cs @@ -14,7 +14,7 @@ public EventsController(IEventRepository eventRepository) { } [HttpGet(Name = nameof(GetEvents))] - public IEnumerable GetEvents(string? start = "", string? end = "") { + public IActionResult GetEvents(string? start = "", string? end = "") { if (!DateTime.TryParse(start, out var startFilter)) { startFilter = DateTime.UnixEpoch; } @@ -23,6 +23,7 @@ public IEnumerable GetEvents(string? start = "", string? end = "") { endFilter = DateTime.Now; } - return eventRepository.FindByCondition(e => e.Start >= startFilter && e.End <= endFilter); + return Ok(eventRepository.FindByCondition(e => e.Start >= startFilter.ToUniversalTime() && e.End <= endFilter + .ToUniversalTime())); } } \ No newline at end of file diff --git a/TescEvents/Properties/launchSettings.json b/TescEvents/Properties/launchSettings.json index 77a4e28..9a29ea0 100644 --- a/TescEvents/Properties/launchSettings.json +++ b/TescEvents/Properties/launchSettings.json @@ -12,7 +12,7 @@ "TescEvents": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", "applicationUrl": "https://localhost:7208;http://localhost:5112", "environmentVariables": { From 5b64214c0088e55afafd9a3bd786e60f3de388dd Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Thu, 6 Oct 2022 22:50:53 -0700 Subject: [PATCH 02/24] Add DB seed --- TescEvents/Entities/RepositoryContext.cs | 2 +- TescEvents/Program.cs | 29 +++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/TescEvents/Entities/RepositoryContext.cs b/TescEvents/Entities/RepositoryContext.cs index 2e432cc..76d1491 100644 --- a/TescEvents/Entities/RepositoryContext.cs +++ b/TescEvents/Entities/RepositoryContext.cs @@ -10,6 +10,6 @@ public RepositoryContext(DbContextOptions options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder options) { options.UseNpgsql(AppSettings.ConnectionString); } - + public DbSet? Events { get; set; } } \ No newline at end of file diff --git a/TescEvents/Program.cs b/TescEvents/Program.cs index 54494c9..44f54d6 100644 --- a/TescEvents/Program.cs +++ b/TescEvents/Program.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using TescEvents.Entities; +using TescEvents.Models; using TescEvents.Repositories; using TescEvents.Utilities; @@ -22,6 +23,8 @@ var app = builder.Build(); +if (app.Environment.IsDevelopment()) SeedDb(); + app.UseHttpsRedirection(); app.UseAuthorization(); @@ -29,4 +32,28 @@ app.MapControllers(); -app.Run(); \ No newline at end of file +app.Run(); + +void SeedDb() { + using var serviceScope = app.Services.GetService().CreateScope(); + var context = serviceScope.ServiceProvider.GetRequiredService(); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + context.Events.AddRange(new Event { + Id = Guid.NewGuid(), + Title = "Event 1", + Start = new DateTime(2021, 5, 23, 9, 0, 0).ToUniversalTime(), + End = new DateTime(2021, 5, 23, 10, 0, 0).ToUniversalTime() + }, new Event { + Id = Guid.NewGuid(), + Title = "Event 2", + Start = new DateTime(2022, 7, 23, 9, 0, 0).ToUniversalTime(), + End = new DateTime(2022, 7, 23, 11, 30, 0).ToUniversalTime() + }, new Event { + Id = Guid.NewGuid(), + Title = "Event 3", + Start = new DateTime(2022, 9, 29, 11, 0, 0).ToUniversalTime(), + End = new DateTime(2022, 9, 23, 14, 0, 0).ToUniversalTime() + }); + context.SaveChanges(); +} \ No newline at end of file From 9cedb8015f64d25990f4de8ca95c1174b07c3e75 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Wed, 12 Oct 2022 10:49:39 -0700 Subject: [PATCH 03/24] Add Automapper --- TescEvents/Controllers/EventsController.cs | 19 ++++++++++++++++--- .../DTOs/Events/EventCreateRequestDTO.cs | 10 ++++++++++ .../DTOs/Events/EventPublicResponseDTO.cs | 10 ++++++++++ TescEvents/Program.cs | 5 +++++ TescEvents/TescEvents.csproj | 4 +++- TescEvents/Utilities/Profiles/EventProfile.cs | 12 ++++++++++++ 6 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 TescEvents/DTOs/Events/EventCreateRequestDTO.cs create mode 100644 TescEvents/DTOs/Events/EventPublicResponseDTO.cs create mode 100644 TescEvents/Utilities/Profiles/EventProfile.cs diff --git a/TescEvents/Controllers/EventsController.cs b/TescEvents/Controllers/EventsController.cs index f38b6b0..de9d540 100644 --- a/TescEvents/Controllers/EventsController.cs +++ b/TescEvents/Controllers/EventsController.cs @@ -1,4 +1,8 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using TescEvents.DTOs; +using TescEvents.DTOs.Events; using TescEvents.Models; using TescEvents.Repositories; @@ -8,22 +12,31 @@ namespace TescEvents.Controllers; [Route("/api/[controller]")] public class EventsController : ControllerBase { private readonly IEventRepository eventRepository; + private readonly IMapper mapper; - public EventsController(IEventRepository eventRepository) { + public EventsController(IEventRepository eventRepository, IMapper mapper) { this.eventRepository = eventRepository; + this.mapper = mapper; } [HttpGet(Name = nameof(GetEvents))] public IActionResult GetEvents(string? start = "", string? end = "") { if (!DateTime.TryParse(start, out var startFilter)) { - startFilter = DateTime.UnixEpoch; + startFilter = DateTime.Now; } if (!DateTime.TryParse(end, out var endFilter)) { - endFilter = DateTime.Now; + endFilter = DateTime.MinValue; } return Ok(eventRepository.FindByCondition(e => e.Start >= startFilter.ToUniversalTime() && e.End <= endFilter .ToUniversalTime())); } + + [Authorize] + [HttpPost(Name = nameof(CreateEvent))] + public Task CreateEvent(EventCreateRequestDTO e) { + + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/TescEvents/DTOs/Events/EventCreateRequestDTO.cs b/TescEvents/DTOs/Events/EventCreateRequestDTO.cs new file mode 100644 index 0000000..d336771 --- /dev/null +++ b/TescEvents/DTOs/Events/EventCreateRequestDTO.cs @@ -0,0 +1,10 @@ +namespace TescEvents.DTOs.Events; + +public class EventCreateRequestDTO { + public string Title { get; set; } + public string Description { get; set; } + public IFormFile? Thumbnail { get; set; } + public IFormFile? Cover { get; set; } + public DateTime Start { get; set; } + public DateTime End { get; set; } +} \ No newline at end of file diff --git a/TescEvents/DTOs/Events/EventPublicResponseDTO.cs b/TescEvents/DTOs/Events/EventPublicResponseDTO.cs new file mode 100644 index 0000000..3802a59 --- /dev/null +++ b/TescEvents/DTOs/Events/EventPublicResponseDTO.cs @@ -0,0 +1,10 @@ +namespace TescEvents.DTOs.Events; + +public class EventPublicResponseDTO { + public string Title { get; set; } + public string Description { get; set; } + public IFormFile? Thumbnail { get; set; } + public IFormFile? Cover { get; set; } + public DateTime Start { get; set; } + public DateTime End { get; set; } +} \ No newline at end of file diff --git a/TescEvents/Program.cs b/TescEvents/Program.cs index 44f54d6..0b911e7 100644 --- a/TescEvents/Program.cs +++ b/TescEvents/Program.cs @@ -1,9 +1,11 @@ +using AutoMapper; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using TescEvents.Entities; using TescEvents.Models; using TescEvents.Repositories; using TescEvents.Utilities; +using TescEvents.Utilities.Profiles; var builder = WebApplication.CreateBuilder(args); @@ -12,6 +14,9 @@ var dotenv = Path.Combine(root, ".env"); DotEnv.Load(dotenv); +// Add Automapper configuration +builder.Services.AddAutoMapper(typeof(EventProfile)); + builder.Services.AddControllers(); builder.Services.AddDbContext(options => options.UseNpgsql(AppSettings.ConnectionString)); diff --git a/TescEvents/TescEvents.csproj b/TescEvents/TescEvents.csproj index 5538ee0..0fbb2a8 100644 --- a/TescEvents/TescEvents.csproj +++ b/TescEvents/TescEvents.csproj @@ -7,7 +7,10 @@ + + + all @@ -20,7 +23,6 @@ - diff --git a/TescEvents/Utilities/Profiles/EventProfile.cs b/TescEvents/Utilities/Profiles/EventProfile.cs new file mode 100644 index 0000000..1941e34 --- /dev/null +++ b/TescEvents/Utilities/Profiles/EventProfile.cs @@ -0,0 +1,12 @@ +using AutoMapper; +using TescEvents.DTOs.Events; +using TescEvents.Models; + +namespace TescEvents.Utilities.Profiles; + +public class EventProfile : Profile { + public EventProfile() { + CreateMap(); + CreateMap(); + } +} \ No newline at end of file From 319beb8dc184208c643b66cf49ec832ea1166fbb Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Wed, 12 Oct 2022 11:04:57 -0700 Subject: [PATCH 04/24] Create event route --- TescEvents/Controllers/EventsController.cs | 17 +++++++++++------ .../DTOs/Events/EventPublicResponseDTO.cs | 1 + TescEvents/Repositories/IRepositoryBase.cs | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/TescEvents/Controllers/EventsController.cs b/TescEvents/Controllers/EventsController.cs index de9d540..69d80de 100644 --- a/TescEvents/Controllers/EventsController.cs +++ b/TescEvents/Controllers/EventsController.cs @@ -26,17 +26,22 @@ public IActionResult GetEvents(string? start = "", string? end = "") { } if (!DateTime.TryParse(end, out var endFilter)) { - endFilter = DateTime.MinValue; + endFilter = DateTime.MaxValue; } - return Ok(eventRepository.FindByCondition(e => e.Start >= startFilter.ToUniversalTime() && e.End <= endFilter - .ToUniversalTime())); + return Ok(eventRepository.FindByCondition(e => e.Start >= startFilter.ToUniversalTime() + && e.End <= endFilter.ToUniversalTime())); } - [Authorize] [HttpPost(Name = nameof(CreateEvent))] - public Task CreateEvent(EventCreateRequestDTO e) { + public async Task CreateEvent(EventCreateRequestDTO e) { + var eventEntity = mapper.Map(e); - throw new NotImplementedException(); + // TODO: Abstract into service transaction and async call + eventRepository.Create(eventEntity); + eventRepository.Save(); + + var eventResponse = mapper.Map(eventEntity); + return CreatedAtRoute(nameof(CreateEvent), new { Id = eventResponse.Id }, eventResponse); } } \ No newline at end of file diff --git a/TescEvents/DTOs/Events/EventPublicResponseDTO.cs b/TescEvents/DTOs/Events/EventPublicResponseDTO.cs index 3802a59..38a8153 100644 --- a/TescEvents/DTOs/Events/EventPublicResponseDTO.cs +++ b/TescEvents/DTOs/Events/EventPublicResponseDTO.cs @@ -1,6 +1,7 @@ namespace TescEvents.DTOs.Events; public class EventPublicResponseDTO { + public string Id { get; set; } public string Title { get; set; } public string Description { get; set; } public IFormFile? Thumbnail { get; set; } diff --git a/TescEvents/Repositories/IRepositoryBase.cs b/TescEvents/Repositories/IRepositoryBase.cs index fdd5ffe..6a0fd95 100644 --- a/TescEvents/Repositories/IRepositoryBase.cs +++ b/TescEvents/Repositories/IRepositoryBase.cs @@ -8,4 +8,5 @@ public interface IRepositoryBase { void Create(T entity); void Update(T entity); void Delete(T entity); + void Save(); } \ No newline at end of file From 54ea90b588a7684ed548a4530f376e624c03f326 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Wed, 12 Oct 2022 11:50:44 -0700 Subject: [PATCH 05/24] Add validators --- TescEvents/Controllers/EventsController.cs | 13 ++++++++++-- .../DTOs/Events/EventCreateRequestDTO.cs | 2 ++ .../DTOs/Events/EventPublicResponseDTO.cs | 4 ++-- TescEvents/Program.cs | 6 ++++++ TescEvents/TescEvents.csproj | 1 + TescEvents/Utilities/Profiles/EventProfile.cs | 4 +++- TescEvents/Validators/EventValidator.cs | 21 +++++++++++++++++++ 7 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 TescEvents/Validators/EventValidator.cs diff --git a/TescEvents/Controllers/EventsController.cs b/TescEvents/Controllers/EventsController.cs index 69d80de..fd14445 100644 --- a/TescEvents/Controllers/EventsController.cs +++ b/TescEvents/Controllers/EventsController.cs @@ -1,4 +1,6 @@ +using System.ComponentModel.DataAnnotations; using AutoMapper; +using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using TescEvents.DTOs; @@ -13,10 +15,12 @@ namespace TescEvents.Controllers; public class EventsController : ControllerBase { private readonly IEventRepository eventRepository; private readonly IMapper mapper; + private readonly IValidator validator; - public EventsController(IEventRepository eventRepository, IMapper mapper) { + public EventsController(IEventRepository eventRepository, IMapper mapper, IValidator validator) { this.eventRepository = eventRepository; this.mapper = mapper; + this.validator = validator; } [HttpGet(Name = nameof(GetEvents))] @@ -34,11 +38,16 @@ public IActionResult GetEvents(string? start = "", string? end = "") { } [HttpPost(Name = nameof(CreateEvent))] - public async Task CreateEvent(EventCreateRequestDTO e) { + public async Task CreateEvent([Required] [FromForm] EventCreateRequestDTO e) { var eventEntity = mapper.Map(e); + var validationResult = await validator.ValidateAsync(eventEntity); + + if (!validationResult.IsValid) return BadRequest( + validationResult.Errors.Select(error => error.ErrorMessage)); // TODO: Abstract into service transaction and async call eventRepository.Create(eventEntity); + // TODO: Upload image to AWS eventRepository.Save(); var eventResponse = mapper.Map(eventEntity); diff --git a/TescEvents/DTOs/Events/EventCreateRequestDTO.cs b/TescEvents/DTOs/Events/EventCreateRequestDTO.cs index d336771..d013232 100644 --- a/TescEvents/DTOs/Events/EventCreateRequestDTO.cs +++ b/TescEvents/DTOs/Events/EventCreateRequestDTO.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace TescEvents.DTOs.Events; public class EventCreateRequestDTO { diff --git a/TescEvents/DTOs/Events/EventPublicResponseDTO.cs b/TescEvents/DTOs/Events/EventPublicResponseDTO.cs index 38a8153..191ddae 100644 --- a/TescEvents/DTOs/Events/EventPublicResponseDTO.cs +++ b/TescEvents/DTOs/Events/EventPublicResponseDTO.cs @@ -4,8 +4,8 @@ public class EventPublicResponseDTO { public string Id { get; set; } public string Title { get; set; } public string Description { get; set; } - public IFormFile? Thumbnail { get; set; } - public IFormFile? Cover { get; set; } + public string Thumbnail { get; set; } + public string Cover { get; set; } public DateTime Start { get; set; } public DateTime End { get; set; } } \ No newline at end of file diff --git a/TescEvents/Program.cs b/TescEvents/Program.cs index 0b911e7..a4a9233 100644 --- a/TescEvents/Program.cs +++ b/TescEvents/Program.cs @@ -1,4 +1,5 @@ using AutoMapper; +using FluentValidation; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using TescEvents.Entities; @@ -6,6 +7,7 @@ using TescEvents.Repositories; using TescEvents.Utilities; using TescEvents.Utilities.Profiles; +using TescEvents.Validators; var builder = WebApplication.CreateBuilder(args); @@ -22,6 +24,10 @@ options.UseNpgsql(AppSettings.ConnectionString)); builder.Services.AddScoped(); + +// Add validators +builder.Services.AddScoped, EventValidator>(); + builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Configuration.AddEnvironmentVariables(); diff --git a/TescEvents/TescEvents.csproj b/TescEvents/TescEvents.csproj index 0fbb2a8..115a024 100644 --- a/TescEvents/TescEvents.csproj +++ b/TescEvents/TescEvents.csproj @@ -9,6 +9,7 @@ + diff --git a/TescEvents/Utilities/Profiles/EventProfile.cs b/TescEvents/Utilities/Profiles/EventProfile.cs index 1941e34..4e7042c 100644 --- a/TescEvents/Utilities/Profiles/EventProfile.cs +++ b/TescEvents/Utilities/Profiles/EventProfile.cs @@ -6,7 +6,9 @@ namespace TescEvents.Utilities.Profiles; public class EventProfile : Profile { public EventProfile() { - CreateMap(); + CreateMap() + .ForMember(e => e.Thumbnail, option => option.Ignore()) + .ForMember(e => e.Cover, option => option.Ignore()); CreateMap(); } } \ No newline at end of file diff --git a/TescEvents/Validators/EventValidator.cs b/TescEvents/Validators/EventValidator.cs new file mode 100644 index 0000000..050ff90 --- /dev/null +++ b/TescEvents/Validators/EventValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using TescEvents.Models; + +namespace TescEvents.Validators; + +public class EventValidator : AbstractValidator { + public EventValidator() { + RuleFor(e => e.Title) + .NotEmpty(); + RuleFor(e => e.Start) + .Cascade(CascadeMode.Stop) + .NotEmpty() + .GreaterThan(DateTime.Now); + RuleFor(e => e.End) + .Cascade(CascadeMode.Stop) + .NotEmpty() + .GreaterThan(e => e.Start); + RuleFor(e => e.Description) + .NotEmpty(); + } +} \ No newline at end of file From 74a8ed67705890e09d975999a5db1800c571301d Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Wed, 12 Oct 2022 17:06:45 -0700 Subject: [PATCH 06/24] Add JWT auth scheme --- TescEvents/Program.cs | 24 ++++++++++++++++++++++-- TescEvents/TescEvents.csproj | 1 + 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/TescEvents/Program.cs b/TescEvents/Program.cs index a4a9233..cf36f10 100644 --- a/TescEvents/Program.cs +++ b/TescEvents/Program.cs @@ -1,7 +1,8 @@ -using AutoMapper; +using System.Text; using FluentValidation; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; using TescEvents.Entities; using TescEvents.Models; using TescEvents.Repositories; @@ -32,6 +33,25 @@ builder.Configuration.AddEnvironmentVariables(); +builder.Services.AddAuthentication(options => { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +}).AddJwtBearer(options => { + options.TokenValidationParameters = new TokenValidationParameters { + ValidIssuer = Environment.GetEnvironmentVariable("JWT_ISSUER"), + ValidAudience = Environment.GetEnvironmentVariable("JWT_AUDIENCE"), + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("JWT_KEY") + ?? throw new InvalidOperationException("JWT_KEY is invalid")) + ), + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = false, + ValidateIssuerSigningKey = true, + }; +}); + var app = builder.Build(); if (app.Environment.IsDevelopment()) SeedDb(); diff --git a/TescEvents/TescEvents.csproj b/TescEvents/TescEvents.csproj index 115a024..ffea109 100644 --- a/TescEvents/TescEvents.csproj +++ b/TescEvents/TescEvents.csproj @@ -10,6 +10,7 @@ + From a3f9b015d31d963787704729dc4d16417d3574e0 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Wed, 12 Oct 2022 22:46:54 -0700 Subject: [PATCH 07/24] Create JWT --- TescEvents/Controllers/AuthController.cs | 42 ++++++++++++++++++++++++ TescEvents/Utilities/AppSettings.cs | 2 ++ 2 files changed, 44 insertions(+) create mode 100644 TescEvents/Controllers/AuthController.cs diff --git a/TescEvents/Controllers/AuthController.cs b/TescEvents/Controllers/AuthController.cs new file mode 100644 index 0000000..7701312 --- /dev/null +++ b/TescEvents/Controllers/AuthController.cs @@ -0,0 +1,42 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using TescEvents.Utilities; + +namespace TescEvents.Controllers; + +[ApiController] +[Route("/api/[controller]")] +public class AuthController : ControllerBase { + public AuthController() { + + } + + [AllowAnonymous] + [HttpPost(Name = nameof(AuthenticateUser))] + public async Task AuthenticateUser([FromForm] string username, [FromForm] string password) { + var issuer = Environment.GetEnvironmentVariable("JWT_ISSUER"); + var audience = Environment.GetEnvironmentVariable("JWT_AUDIENCE"); + var key = Encoding.ASCII.GetBytes(Environment.GetEnvironmentVariable("JWT_KEY")); + var tokenDescriptor = new SecurityTokenDescriptor { + Subject = new ClaimsIdentity(new[] { + new Claim(JwtRegisteredClaimNames.Sub, username), + new Claim(JwtRegisteredClaimNames.Jti, new Guid().ToString()), + }), + Expires = DateTime.UtcNow.AddMinutes(AppSettings.VALID_JWT_LENGTH_DAYS), + Issuer = issuer, + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey(key), + SecurityAlgorithms.HmacSha512Signature) + }; + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateToken(tokenDescriptor); + var jwt = tokenHandler.WriteToken(token); + return Ok(jwt); + + return Unauthorized(); + } +} \ No newline at end of file diff --git a/TescEvents/Utilities/AppSettings.cs b/TescEvents/Utilities/AppSettings.cs index b2712da..d20e5ce 100644 --- a/TescEvents/Utilities/AppSettings.cs +++ b/TescEvents/Utilities/AppSettings.cs @@ -11,4 +11,6 @@ public static string ConnectionString { return $"Host={host};Port={port};Database={database};Username={user};Password={pass}"; } } + + public const int VALID_JWT_LENGTH_DAYS = 14; } \ No newline at end of file From f3697be720935d2ebbeec465e39ba6dd00ae820a Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Wed, 12 Oct 2022 23:21:27 -0700 Subject: [PATCH 08/24] Add Users table --- TescEvents/Entities/RepositoryContext.cs | 1 + .../Migrations/0002-AddUsersTable.Designer.cs | 99 +++++++++++++++++++ TescEvents/Migrations/0002-AddUsersTable.cs | 36 +++++++ .../RepositoryContextModelSnapshot.cs | 35 +++++++ TescEvents/Models/User.cs | 35 +++++++ 5 files changed, 206 insertions(+) create mode 100644 TescEvents/Migrations/0002-AddUsersTable.Designer.cs create mode 100644 TescEvents/Migrations/0002-AddUsersTable.cs create mode 100644 TescEvents/Models/User.cs diff --git a/TescEvents/Entities/RepositoryContext.cs b/TescEvents/Entities/RepositoryContext.cs index 76d1491..546765a 100644 --- a/TescEvents/Entities/RepositoryContext.cs +++ b/TescEvents/Entities/RepositoryContext.cs @@ -12,4 +12,5 @@ protected override void OnConfiguring(DbContextOptionsBuilder options) { } public DbSet? Events { get; set; } + public DbSet? Users { get; set; } } \ No newline at end of file diff --git a/TescEvents/Migrations/0002-AddUsersTable.Designer.cs b/TescEvents/Migrations/0002-AddUsersTable.Designer.cs new file mode 100644 index 0000000..a7c48b8 --- /dev/null +++ b/TescEvents/Migrations/0002-AddUsersTable.Designer.cs @@ -0,0 +1,99 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TescEvents.Entities; + +#nullable disable + +namespace TescEvents.Migrations +{ + [DbContext(typeof(RepositoryContext))] + [Migration("20221013062041_0002-AddUsersTable")] + partial class _0002AddUsersTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TescEvents.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Archived") + .HasColumnType("boolean"); + + b.Property("Cover") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("Thumbnail") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TescEvents.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TescEvents/Migrations/0002-AddUsersTable.cs b/TescEvents/Migrations/0002-AddUsersTable.cs new file mode 100644 index 0000000..83bca8f --- /dev/null +++ b/TescEvents/Migrations/0002-AddUsersTable.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TescEvents.Migrations +{ + public partial class _0002AddUsersTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Username = table.Column(type: "text", nullable: false), + FirstName = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + Salt = table.Column(type: "text", nullable: false), + UserType = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs index 7901737..267b043 100644 --- a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs +++ b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs @@ -56,6 +56,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Events"); }); + + modelBuilder.Entity("TescEvents.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); #pragma warning restore 612, 618 } } diff --git a/TescEvents/Models/User.cs b/TescEvents/Models/User.cs new file mode 100644 index 0000000..507751d --- /dev/null +++ b/TescEvents/Models/User.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace TescEvents.Models; + +[Table("Users")] +public class User { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + public Guid Id { get; set; } + + [Required] + public string Username { get; set; } + + [Required] + public string FirstName { get; set; } + + [Required] + public string LastName { get; set; } + + [Required] + public string PasswordHash { get; set; } + + [Required] + public string Salt { get; set; } + + [Required] + public string UserType { get; set; } = UserTypes.REGULAR; +} + +public class UserTypes { + public const string REGULAR = "REGULAR"; + public const string ADMIN = "ADMIN"; + public const string COORDINATOR = "COORDINATOR"; +} \ No newline at end of file From 10c111444b32d438d9955bec13a32920c2e5a1b9 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Thu, 13 Oct 2022 20:35:22 -0700 Subject: [PATCH 09/24] Index by users --- .../0003-IndexUserByUsername.Designer.cs | 102 ++++++++++++++++++ .../Migrations/0003-IndexUserByUsername.cs | 25 +++++ .../RepositoryContextModelSnapshot.cs | 3 + TescEvents/Models/User.cs | 2 + 4 files changed, 132 insertions(+) create mode 100644 TescEvents/Migrations/0003-IndexUserByUsername.Designer.cs create mode 100644 TescEvents/Migrations/0003-IndexUserByUsername.cs diff --git a/TescEvents/Migrations/0003-IndexUserByUsername.Designer.cs b/TescEvents/Migrations/0003-IndexUserByUsername.Designer.cs new file mode 100644 index 0000000..f94788d --- /dev/null +++ b/TescEvents/Migrations/0003-IndexUserByUsername.Designer.cs @@ -0,0 +1,102 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TescEvents.Entities; + +#nullable disable + +namespace TescEvents.Migrations +{ + [DbContext(typeof(RepositoryContext))] + [Migration("20221014033420_0003-IndexUserByUsername")] + partial class _0003IndexUserByUsername + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TescEvents.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Archived") + .HasColumnType("boolean"); + + b.Property("Cover") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("Thumbnail") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TescEvents.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TescEvents/Migrations/0003-IndexUserByUsername.cs b/TescEvents/Migrations/0003-IndexUserByUsername.cs new file mode 100644 index 0000000..4d1ab04 --- /dev/null +++ b/TescEvents/Migrations/0003-IndexUserByUsername.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TescEvents.Migrations +{ + public partial class _0003IndexUserByUsername : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Users_Username", + table: "Users"); + } + } +} diff --git a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs index 267b043..49df32a 100644 --- a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs +++ b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs @@ -89,6 +89,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("Username") + .IsUnique(); + b.ToTable("Users"); }); #pragma warning restore 612, 618 diff --git a/TescEvents/Models/User.cs b/TescEvents/Models/User.cs index 507751d..39c8625 100644 --- a/TescEvents/Models/User.cs +++ b/TescEvents/Models/User.cs @@ -1,9 +1,11 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; namespace TescEvents.Models; [Table("Users")] +[Index(nameof(Username), IsUnique = true)] public class User { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Key] From 3103e337d1804eceab2781e3ae348e1074b52292 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Thu, 13 Oct 2022 21:14:42 -0700 Subject: [PATCH 10/24] User auth --- TescEvents/Controllers/AuthController.cs | 22 +++++++++++++++------- TescEvents/Program.cs | 10 ++++++++++ TescEvents/Repositories/IUserRepository.cs | 12 ++++++++++++ TescEvents/Repositories/UserRepository.cs | 14 ++++++++++++++ TescEvents/TescEvents.csproj | 1 + 5 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 TescEvents/Repositories/IUserRepository.cs create mode 100644 TescEvents/Repositories/UserRepository.cs diff --git a/TescEvents/Controllers/AuthController.cs b/TescEvents/Controllers/AuthController.cs index 7701312..3d40e51 100644 --- a/TescEvents/Controllers/AuthController.cs +++ b/TescEvents/Controllers/AuthController.cs @@ -4,39 +4,47 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; +using TescEvents.Models; +using TescEvents.Repositories; using TescEvents.Utilities; +using static BCrypt.Net.BCrypt; namespace TescEvents.Controllers; [ApiController] [Route("/api/[controller]")] public class AuthController : ControllerBase { - public AuthController() { - + private readonly IUserRepository userRepository; + public AuthController(IUserRepository userRepository) { + this.userRepository = userRepository; } [AllowAnonymous] [HttpPost(Name = nameof(AuthenticateUser))] public async Task AuthenticateUser([FromForm] string username, [FromForm] string password) { + var user = userRepository.GetUserByUsername(username); + if (user == null) return Unauthorized(); + + if (HashPassword(password, user.Salt) != user.PasswordHash) return Unauthorized(); + var issuer = Environment.GetEnvironmentVariable("JWT_ISSUER"); var audience = Environment.GetEnvironmentVariable("JWT_AUDIENCE"); - var key = Encoding.ASCII.GetBytes(Environment.GetEnvironmentVariable("JWT_KEY")); + var key = Encoding.ASCII.GetBytes(Environment.GetEnvironmentVariable("JWT_KEY")!); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { - new Claim(JwtRegisteredClaimNames.Sub, username), + new Claim(JwtRegisteredClaimNames.Sub, user.Username), new Claim(JwtRegisteredClaimNames.Jti, new Guid().ToString()), }), Expires = DateTime.UtcNow.AddMinutes(AppSettings.VALID_JWT_LENGTH_DAYS), Issuer = issuer, + Audience = audience, SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), - SecurityAlgorithms.HmacSha512Signature) + SecurityAlgorithms.HmacSha512Signature), }; var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken(tokenDescriptor); var jwt = tokenHandler.WriteToken(token); return Ok(jwt); - - return Unauthorized(); } } \ No newline at end of file diff --git a/TescEvents/Program.cs b/TescEvents/Program.cs index cf36f10..875b667 100644 --- a/TescEvents/Program.cs +++ b/TescEvents/Program.cs @@ -86,5 +86,15 @@ void SeedDb() { Start = new DateTime(2022, 9, 29, 11, 0, 0).ToUniversalTime(), End = new DateTime(2022, 9, 23, 14, 0, 0).ToUniversalTime() }); + + context.Users.AddRange(new User { + Id = Guid.NewGuid(), + Username = "sek007@ucsd.edu", + FirstName = "Shane", + LastName = "Kim", + PasswordHash = "", + Salt = "reallygoodsalt", + UserType = UserTypes.REGULAR + }); context.SaveChanges(); } \ No newline at end of file diff --git a/TescEvents/Repositories/IUserRepository.cs b/TescEvents/Repositories/IUserRepository.cs new file mode 100644 index 0000000..484c5c9 --- /dev/null +++ b/TescEvents/Repositories/IUserRepository.cs @@ -0,0 +1,12 @@ +using TescEvents.Models; + +namespace TescEvents.Repositories; + +public interface IUserRepository : IRepositoryBase { + /// + /// Returns the user entity matching the username, or null if not found + /// + /// Username of user + /// User matching username, or null if not found + User? GetUserByUsername(string username); +} \ No newline at end of file diff --git a/TescEvents/Repositories/UserRepository.cs b/TescEvents/Repositories/UserRepository.cs new file mode 100644 index 0000000..2d1ec6e --- /dev/null +++ b/TescEvents/Repositories/UserRepository.cs @@ -0,0 +1,14 @@ +using TescEvents.Entities; +using TescEvents.Models; + +namespace TescEvents.Repositories; + +public class UserRepository : RepositoryBase, IUserRepository { + public UserRepository(RepositoryContext context) : base(context) { + } + + public User? GetUserByUsername(string username) { + return FindByCondition(user => user.Username == username) + .FirstOrDefault(); + } +} \ No newline at end of file diff --git a/TescEvents/TescEvents.csproj b/TescEvents/TescEvents.csproj index ffea109..b4871de 100644 --- a/TescEvents/TescEvents.csproj +++ b/TescEvents/TescEvents.csproj @@ -9,6 +9,7 @@ + From a673e2c606e62fdcb2e31ec9589bbb3e5b672e05 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Thu, 13 Oct 2022 21:58:06 -0700 Subject: [PATCH 11/24] User register --- TescEvents/Controllers/AuthController.cs | 42 ++++++++++++++++++- TescEvents/DTOs/Users/UserCreateRequestDTO.cs | 8 ++++ TescEvents/DTOs/Users/UserResponseDTO.cs | 9 ++++ TescEvents/Program.cs | 4 +- TescEvents/Repositories/IUserRepository.cs | 13 ++++++ TescEvents/Repositories/UserRepository.cs | 6 +++ TescEvents/Utilities/Profiles/UserProfile.cs | 16 +++++++ TescEvents/Validators/UserValidator.cs | 10 +++++ 8 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 TescEvents/DTOs/Users/UserCreateRequestDTO.cs create mode 100644 TescEvents/DTOs/Users/UserResponseDTO.cs create mode 100644 TescEvents/Utilities/Profiles/UserProfile.cs create mode 100644 TescEvents/Validators/UserValidator.cs diff --git a/TescEvents/Controllers/AuthController.cs b/TescEvents/Controllers/AuthController.cs index 3d40e51..43945e7 100644 --- a/TescEvents/Controllers/AuthController.cs +++ b/TescEvents/Controllers/AuthController.cs @@ -1,13 +1,19 @@ +using System.ComponentModel.DataAnnotations; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; +using AutoMapper; +using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; +using TescEvents.DTOs; +using TescEvents.DTOs.Users; using TescEvents.Models; using TescEvents.Repositories; using TescEvents.Utilities; using static BCrypt.Net.BCrypt; +using ValidationResult = FluentValidation.Results.ValidationResult; namespace TescEvents.Controllers; @@ -15,8 +21,42 @@ namespace TescEvents.Controllers; [Route("/api/[controller]")] public class AuthController : ControllerBase { private readonly IUserRepository userRepository; - public AuthController(IUserRepository userRepository) { + private readonly IValidator userValidator; + private readonly IMapper mapper; + + public AuthController(IUserRepository userRepository, IMapper mapper, IValidator userValidator) { this.userRepository = userRepository; + this.mapper = mapper; + this.userValidator = userValidator; + } + + [AllowAnonymous] + [HttpPost(Name = nameof(RegisterUser))] + public async Task RegisterUser([Required] [FromForm] UserCreateRequestDTO userReq) { + var userEntity = mapper.Map(userReq); + + var validationResult = await userValidator.ValidateAsync(userEntity); + if (!validationResult.IsValid) return BadRequest( + validationResult.Errors + .Select(error => error.ErrorMessage)); + + var salt = GenerateSalt(); + userEntity.Salt = salt; + userEntity.PasswordHash = HashPassword(userReq.Password, salt); + userRepository.CreateUser(userEntity); + + var userResponse = mapper.Map(userEntity); + return CreatedAtRoute(nameof(GetUser), new { Id = userResponse.Id }, userResponse); + } + + [AllowAnonymous] + [HttpGet("/user/{uuid}", Name = nameof(GetUser))] + public async Task GetUser(string uuid) { + var user = userRepository.GetUserByUuid(uuid); + if (user == null) return NotFound(); + + var userResponse = mapper.Map(user); + return Ok(userResponse); } [AllowAnonymous] diff --git a/TescEvents/DTOs/Users/UserCreateRequestDTO.cs b/TescEvents/DTOs/Users/UserCreateRequestDTO.cs new file mode 100644 index 0000000..0556d0f --- /dev/null +++ b/TescEvents/DTOs/Users/UserCreateRequestDTO.cs @@ -0,0 +1,8 @@ +namespace TescEvents.DTOs.Users; + +public class UserCreateRequestDTO { + public string Username { get; set; } + public string Password { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } +} \ No newline at end of file diff --git a/TescEvents/DTOs/Users/UserResponseDTO.cs b/TescEvents/DTOs/Users/UserResponseDTO.cs new file mode 100644 index 0000000..340739c --- /dev/null +++ b/TescEvents/DTOs/Users/UserResponseDTO.cs @@ -0,0 +1,9 @@ +namespace TescEvents.DTOs.Users; + +public class UserResponseDTO { + public Guid Id { get; set; } + public string Username { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string UserType { get; set; } +} \ No newline at end of file diff --git a/TescEvents/Program.cs b/TescEvents/Program.cs index 875b667..d5b0b7e 100644 --- a/TescEvents/Program.cs +++ b/TescEvents/Program.cs @@ -18,16 +18,18 @@ DotEnv.Load(dotenv); // Add Automapper configuration -builder.Services.AddAutoMapper(typeof(EventProfile)); +builder.Services.AddAutoMapper(typeof(EventProfile), typeof(UserProfile)); builder.Services.AddControllers(); builder.Services.AddDbContext(options => options.UseNpgsql(AppSettings.ConnectionString)); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Add validators builder.Services.AddScoped, EventValidator>(); +builder.Services.AddScoped, UserValidator>(); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); diff --git a/TescEvents/Repositories/IUserRepository.cs b/TescEvents/Repositories/IUserRepository.cs index 484c5c9..c0a8de0 100644 --- a/TescEvents/Repositories/IUserRepository.cs +++ b/TescEvents/Repositories/IUserRepository.cs @@ -9,4 +9,17 @@ public interface IUserRepository : IRepositoryBase { /// Username of user /// User matching username, or null if not found User? GetUserByUsername(string username); + + /// + /// Returns the user entity matching the uuid, or null if not found + /// + /// + /// User matching uuid, or null if not found + User? GetUserByUuid(string uuid); + + /// + /// Inserts a User entity in the database + /// User to be created + /// + void CreateUser(User user); } \ No newline at end of file diff --git a/TescEvents/Repositories/UserRepository.cs b/TescEvents/Repositories/UserRepository.cs index 2d1ec6e..9538cba 100644 --- a/TescEvents/Repositories/UserRepository.cs +++ b/TescEvents/Repositories/UserRepository.cs @@ -11,4 +11,10 @@ public UserRepository(RepositoryContext context) : base(context) { return FindByCondition(user => user.Username == username) .FirstOrDefault(); } + + public void CreateUser(User user) { + Create(user); + Save(); + } + } \ No newline at end of file diff --git a/TescEvents/Utilities/Profiles/UserProfile.cs b/TescEvents/Utilities/Profiles/UserProfile.cs new file mode 100644 index 0000000..de51e40 --- /dev/null +++ b/TescEvents/Utilities/Profiles/UserProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using TescEvents.DTOs; +using TescEvents.DTOs.Users; +using TescEvents.Models; + +namespace TescEvents.Utilities.Profiles; + +public class UserProfile : Profile { + public UserProfile() { + CreateMap() + .ForMember(u => u.PasswordHash, option => option.Ignore()) + .ForMember(u => u.Salt, option => option.Ignore()); + + CreateMap(); + } +} \ No newline at end of file diff --git a/TescEvents/Validators/UserValidator.cs b/TescEvents/Validators/UserValidator.cs new file mode 100644 index 0000000..51893e2 --- /dev/null +++ b/TescEvents/Validators/UserValidator.cs @@ -0,0 +1,10 @@ +using FluentValidation; +using TescEvents.Models; + +namespace TescEvents.Validators; + +public class UserValidator : AbstractValidator { + public UserValidator() { + // TODO: Insert user validation rules + } +} \ No newline at end of file From 020c1383e8ac3e7e5c28374b1e5fe61b280d9ef8 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Thu, 13 Oct 2022 22:03:05 -0700 Subject: [PATCH 12/24] User id validation --- TescEvents/Controllers/AuthController.cs | 2 +- TescEvents/Repositories/IUserRepository.cs | 2 +- TescEvents/Repositories/UserRepository.cs | 5 +++++ TescEvents/Validators/UserValidator.cs | 5 ++++- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/TescEvents/Controllers/AuthController.cs b/TescEvents/Controllers/AuthController.cs index 43945e7..87e2eae 100644 --- a/TescEvents/Controllers/AuthController.cs +++ b/TescEvents/Controllers/AuthController.cs @@ -52,7 +52,7 @@ public async Task RegisterUser([Required] [FromForm] UserCreateRe [AllowAnonymous] [HttpGet("/user/{uuid}", Name = nameof(GetUser))] public async Task GetUser(string uuid) { - var user = userRepository.GetUserByUuid(uuid); + var user = userRepository.GetUserByUuid(Guid.Parse(uuid)); if (user == null) return NotFound(); var userResponse = mapper.Map(user); diff --git a/TescEvents/Repositories/IUserRepository.cs b/TescEvents/Repositories/IUserRepository.cs index c0a8de0..cbf598f 100644 --- a/TescEvents/Repositories/IUserRepository.cs +++ b/TescEvents/Repositories/IUserRepository.cs @@ -15,7 +15,7 @@ public interface IUserRepository : IRepositoryBase { /// /// /// User matching uuid, or null if not found - User? GetUserByUuid(string uuid); + User? GetUserByUuid(Guid uuid); /// /// Inserts a User entity in the database diff --git a/TescEvents/Repositories/UserRepository.cs b/TescEvents/Repositories/UserRepository.cs index 9538cba..ea5b9fb 100644 --- a/TescEvents/Repositories/UserRepository.cs +++ b/TescEvents/Repositories/UserRepository.cs @@ -12,6 +12,11 @@ public UserRepository(RepositoryContext context) : base(context) { .FirstOrDefault(); } + public User? GetUserByUuid(Guid uuid) { + return FindByCondition(user => user.Id == uuid) + .FirstOrDefault(); + } + public void CreateUser(User user) { Create(user); Save(); diff --git a/TescEvents/Validators/UserValidator.cs b/TescEvents/Validators/UserValidator.cs index 51893e2..d0d06a6 100644 --- a/TescEvents/Validators/UserValidator.cs +++ b/TescEvents/Validators/UserValidator.cs @@ -1,10 +1,13 @@ using FluentValidation; using TescEvents.Models; +using TescEvents.Repositories; namespace TescEvents.Validators; public class UserValidator : AbstractValidator { - public UserValidator() { + public UserValidator(IUserRepository userRepository) { // TODO: Insert user validation rules + RuleFor(u => userRepository.GetUserByUuid(u.Id)) + .Null(); } } \ No newline at end of file From 61719569db1252105b67e49c29ac5664cfa58dd7 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Thu, 13 Oct 2022 22:07:40 -0700 Subject: [PATCH 13/24] Fix user validation --- TescEvents/Validators/UserValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TescEvents/Validators/UserValidator.cs b/TescEvents/Validators/UserValidator.cs index d0d06a6..0a828f2 100644 --- a/TescEvents/Validators/UserValidator.cs +++ b/TescEvents/Validators/UserValidator.cs @@ -7,7 +7,7 @@ namespace TescEvents.Validators; public class UserValidator : AbstractValidator { public UserValidator(IUserRepository userRepository) { // TODO: Insert user validation rules - RuleFor(u => userRepository.GetUserByUuid(u.Id)) + RuleFor(u => userRepository.GetUserByUsername(u.Username)) .Null(); } } \ No newline at end of file From 0a93d201b45e13d9031fec4d90cf1e223e858166 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Thu, 13 Oct 2022 22:57:52 -0700 Subject: [PATCH 14/24] Create users controller and validate username --- TescEvents/Controllers/AuthController.cs | 19 ++++++-------- TescEvents/Controllers/UsersController.cs | 30 +++++++++++++++++++++++ TescEvents/Validators/UserValidator.cs | 7 +++--- 3 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 TescEvents/Controllers/UsersController.cs diff --git a/TescEvents/Controllers/AuthController.cs b/TescEvents/Controllers/AuthController.cs index 87e2eae..01c8389 100644 --- a/TescEvents/Controllers/AuthController.cs +++ b/TescEvents/Controllers/AuthController.cs @@ -31,7 +31,7 @@ public AuthController(IUserRepository userRepository, IMapper mapper, IValidator } [AllowAnonymous] - [HttpPost(Name = nameof(RegisterUser))] + [HttpPost("register", Name = nameof(RegisterUser))] public async Task RegisterUser([Required] [FromForm] UserCreateRequestDTO userReq) { var userEntity = mapper.Map(userReq); @@ -46,17 +46,12 @@ public async Task RegisterUser([Required] [FromForm] UserCreateRe userRepository.CreateUser(userEntity); var userResponse = mapper.Map(userEntity); - return CreatedAtRoute(nameof(GetUser), new { Id = userResponse.Id }, userResponse); - } - - [AllowAnonymous] - [HttpGet("/user/{uuid}", Name = nameof(GetUser))] - public async Task GetUser(string uuid) { - var user = userRepository.GetUserByUuid(Guid.Parse(uuid)); - if (user == null) return NotFound(); - - var userResponse = mapper.Map(user); - return Ok(userResponse); + return CreatedAtRoute(new { + action = "GetUser", + controller = "Users", + userResponse.Id + }, + userResponse); } [AllowAnonymous] diff --git a/TescEvents/Controllers/UsersController.cs b/TescEvents/Controllers/UsersController.cs new file mode 100644 index 0000000..4fedaed --- /dev/null +++ b/TescEvents/Controllers/UsersController.cs @@ -0,0 +1,30 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TescEvents.DTOs.Users; +using TescEvents.Repositories; + +namespace TescEvents.Controllers; + +[ApiController] +[Route("/api/[controller]")] +public class UsersController : ControllerBase { + private readonly IUserRepository userRepository; + private readonly IMapper mapper; + + public UsersController(IUserRepository userRepository, IMapper mapper) { + this.userRepository = userRepository; + this.mapper = mapper; + } + + [AllowAnonymous] + [HttpGet(Name = nameof(GetUser))] + [Route("user/{uuid}")] + public async Task GetUser(string uuid) { + var user = userRepository.GetUserByUuid(Guid.Parse(uuid)); + if (user == null) return NotFound(); + + var userResponse = mapper.Map(user); + return Ok(userResponse); + } +} \ No newline at end of file diff --git a/TescEvents/Validators/UserValidator.cs b/TescEvents/Validators/UserValidator.cs index 0a828f2..88cc83d 100644 --- a/TescEvents/Validators/UserValidator.cs +++ b/TescEvents/Validators/UserValidator.cs @@ -6,8 +6,9 @@ namespace TescEvents.Validators; public class UserValidator : AbstractValidator { public UserValidator(IUserRepository userRepository) { - // TODO: Insert user validation rules - RuleFor(u => userRepository.GetUserByUsername(u.Username)) - .Null(); + RuleFor(u => u.Username) + .EmailAddress() + .Must(u => userRepository.GetUserByUsername(u) == null) + .WithMessage("User already exists with that username"); } } \ No newline at end of file From 65ede7f65d8b74e50f328b8666ce863c2fb3b551 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Thu, 13 Oct 2022 23:11:07 -0700 Subject: [PATCH 15/24] Give 404 on auth nonexistent user --- TescEvents/Controllers/AuthController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TescEvents/Controllers/AuthController.cs b/TescEvents/Controllers/AuthController.cs index 01c8389..0903572 100644 --- a/TescEvents/Controllers/AuthController.cs +++ b/TescEvents/Controllers/AuthController.cs @@ -58,7 +58,7 @@ public async Task RegisterUser([Required] [FromForm] UserCreateRe [HttpPost(Name = nameof(AuthenticateUser))] public async Task AuthenticateUser([FromForm] string username, [FromForm] string password) { var user = userRepository.GetUserByUsername(username); - if (user == null) return Unauthorized(); + if (user == null) return NotFound(); if (HashPassword(password, user.Salt) != user.PasswordHash) return Unauthorized(); From 629839b57581ca2d0370f02e272d9feb91620743 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Sat, 22 Oct 2022 23:44:30 -0700 Subject: [PATCH 16/24] Remake migrations --- .../0001-AddEventsTable.Designer.cs | 4 +- TescEvents/Migrations/0001-AddEventsTable.cs | 44 +++++------ .../0002-AddApplicationColumns.Designer.cs | 76 +++++++++++++++++++ .../Migrations/0002-AddApplicationColumns.cs | 58 ++++++++++++++ .../RepositoryContextModelSnapshot.cs | 12 +++ TescEvents/Models/Event.cs | 8 ++ 6 files changed, 176 insertions(+), 26 deletions(-) create mode 100644 TescEvents/Migrations/0002-AddApplicationColumns.Designer.cs create mode 100644 TescEvents/Migrations/0002-AddApplicationColumns.cs diff --git a/TescEvents/Migrations/0001-AddEventsTable.Designer.cs b/TescEvents/Migrations/0001-AddEventsTable.Designer.cs index 49dc51d..004d8be 100644 --- a/TescEvents/Migrations/0001-AddEventsTable.Designer.cs +++ b/TescEvents/Migrations/0001-AddEventsTable.Designer.cs @@ -12,8 +12,8 @@ namespace TescEvents.Migrations { [DbContext(typeof(RepositoryContext))] - [Migration("20221007021603_0001-AddEventsTable")] - partial class _0001AddEventsTable + [Migration("0001-AddEventsTable")] + partial class AddEventsTable { protected override void BuildTargetModel(ModelBuilder modelBuilder) { diff --git a/TescEvents/Migrations/0001-AddEventsTable.cs b/TescEvents/Migrations/0001-AddEventsTable.cs index 2ab3408..7cd5c51 100644 --- a/TescEvents/Migrations/0001-AddEventsTable.cs +++ b/TescEvents/Migrations/0001-AddEventsTable.cs @@ -1,37 +1,33 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace TescEvents.Migrations { - public partial class _0001AddEventsTable : Migration + public partial class AddEventsTable : Migration { - protected override void Up(MigrationBuilder migrationBuilder) - { + protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( - name: "Events", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - Description = table.Column(type: "text", nullable: false), - Thumbnail = table.Column(type: "text", nullable: true), - Cover = table.Column(type: "text", nullable: true), - Start = table.Column(type: "timestamp with time zone", nullable: false), - End = table.Column(type: "timestamp with time zone", nullable: false), - Archived = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Events", x => x.Id); - }); + name: "Events", + columns: table => new { + Id = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "character varying(255)", + maxLength: 255, nullable: false), + Description = table.Column(type: "text", nullable: false), + Thumbnail = table.Column(type: "text", nullable: true), + Cover = table.Column(type: "text", nullable: true), + Start = table.Column(type: "timestamp with time zone", + nullable: false), + End = table.Column(type: "timestamp with time zone", + nullable: false), + Archived = table.Column(type: "boolean", nullable: false) + }, + constraints: table => { table.PrimaryKey("PK_Events", x => x.Id); }); } - protected override void Down(MigrationBuilder migrationBuilder) - { + protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "Events"); + name: "Events"); } } } diff --git a/TescEvents/Migrations/0002-AddApplicationColumns.Designer.cs b/TescEvents/Migrations/0002-AddApplicationColumns.Designer.cs new file mode 100644 index 0000000..5f9cc91 --- /dev/null +++ b/TescEvents/Migrations/0002-AddApplicationColumns.Designer.cs @@ -0,0 +1,76 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TescEvents.Entities; + +#nullable disable + +namespace TescEvents.Migrations +{ + [DbContext(typeof(RepositoryContext))] + [Migration("0002-AddApplicationColumns")] + partial class AddApplicationColumns + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TescEvents.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptingApplications") + .HasColumnType("boolean"); + + b.Property("ApplicationCloseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApplicationOpenDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Archived") + .HasColumnType("boolean"); + + b.Property("Cover") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.Property("RequiresApplication") + .HasColumnType("boolean"); + + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("Thumbnail") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Events"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TescEvents/Migrations/0002-AddApplicationColumns.cs b/TescEvents/Migrations/0002-AddApplicationColumns.cs new file mode 100644 index 0000000..efb7e7a --- /dev/null +++ b/TescEvents/Migrations/0002-AddApplicationColumns.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TescEvents.Migrations +{ + public partial class AddApplicationColumns : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AcceptingApplications", + table: "Events", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ApplicationCloseDate", + table: "Events", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "ApplicationOpenDate", + table: "Events", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "RequiresApplication", + table: "Events", + type: "boolean", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AcceptingApplications", + table: "Events"); + + migrationBuilder.DropColumn( + name: "ApplicationCloseDate", + table: "Events"); + + migrationBuilder.DropColumn( + name: "ApplicationOpenDate", + table: "Events"); + + migrationBuilder.DropColumn( + name: "RequiresApplication", + table: "Events"); + } + } +} diff --git a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs index 7901737..e627166 100644 --- a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs +++ b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs @@ -28,6 +28,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AcceptingApplications") + .HasColumnType("boolean"); + + b.Property("ApplicationCloseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApplicationOpenDate") + .HasColumnType("timestamp with time zone"); + b.Property("Archived") .HasColumnType("boolean"); @@ -41,6 +50,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("End") .HasColumnType("timestamp with time zone"); + b.Property("RequiresApplication") + .HasColumnType("boolean"); + b.Property("Start") .HasColumnType("timestamp with time zone"); diff --git a/TescEvents/Models/Event.cs b/TescEvents/Models/Event.cs index 61d1ae0..63eb939 100644 --- a/TescEvents/Models/Event.cs +++ b/TescEvents/Models/Event.cs @@ -27,4 +27,12 @@ public class Event { public DateTime End { get; set; } public bool Archived { get; set; } = false; + + public bool RequiresApplication { get; set; } = false; + + public DateTime? ApplicationOpenDate { get; set; } + + public DateTime? ApplicationCloseDate { get; set; } + + public bool AcceptingApplications { get; set; } = false; } \ No newline at end of file From 0590b02e4d7138c9ca372676bb7f946a77e0c557 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Sun, 23 Oct 2022 00:26:12 -0700 Subject: [PATCH 17/24] Add event registrations table --- TescEvents/Entities/RepositoryContext.cs | 2 + ...003-AddEventRegistrationsTable.Designer.cs | 135 ++++++++++++++++++ .../0003-AddEventRegistrationsTable.cs | 71 +++++++++ .../RepositoryContextModelSnapshot.cs | 61 +++++++- TescEvents/Models/EventRegistrations.cs | 34 +++++ TescEvents/Models/Student.cs | 11 ++ 6 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 TescEvents/Migrations/0003-AddEventRegistrationsTable.Designer.cs create mode 100644 TescEvents/Migrations/0003-AddEventRegistrationsTable.cs create mode 100644 TescEvents/Models/EventRegistrations.cs create mode 100644 TescEvents/Models/Student.cs diff --git a/TescEvents/Entities/RepositoryContext.cs b/TescEvents/Entities/RepositoryContext.cs index 76d1491..a1d416f 100644 --- a/TescEvents/Entities/RepositoryContext.cs +++ b/TescEvents/Entities/RepositoryContext.cs @@ -12,4 +12,6 @@ protected override void OnConfiguring(DbContextOptionsBuilder options) { } public DbSet? Events { get; set; } + public DbSet? EventRegistrations { get; set; } + public DbSet? Students { get; set; } } \ No newline at end of file diff --git a/TescEvents/Migrations/0003-AddEventRegistrationsTable.Designer.cs b/TescEvents/Migrations/0003-AddEventRegistrationsTable.Designer.cs new file mode 100644 index 0000000..21f04fd --- /dev/null +++ b/TescEvents/Migrations/0003-AddEventRegistrationsTable.Designer.cs @@ -0,0 +1,135 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TescEvents.Entities; + +#nullable disable + +namespace TescEvents.Migrations +{ + [DbContext(typeof(RepositoryContext))] + [Migration("0003-AddEventRegistrationsTable")] + partial class AddEventRegistrationsTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TescEvents.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptingApplications") + .HasColumnType("boolean"); + + b.Property("ApplicationCloseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApplicationOpenDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Archived") + .HasColumnType("boolean"); + + b.Property("Cover") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.Property("RequiresApplication") + .HasColumnType("boolean"); + + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("Thumbnail") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TescEvents.Models.EventRegistrations", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("IsResumeSanitized") + .HasColumnType("boolean"); + + b.Property("StudentId") + .HasColumnType("uuid"); + + b.Property("UserStatus") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("StudentId"); + + b.ToTable("EventRegistrations"); + }); + + modelBuilder.Entity("TescEvents.Models.Student", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Students"); + }); + + modelBuilder.Entity("TescEvents.Models.EventRegistrations", b => + { + b.HasOne("TescEvents.Models.Event", "Event") + .WithMany() + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TescEvents.Models.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + + b.Navigation("Student"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TescEvents/Migrations/0003-AddEventRegistrationsTable.cs b/TescEvents/Migrations/0003-AddEventRegistrationsTable.cs new file mode 100644 index 0000000..a202246 --- /dev/null +++ b/TescEvents/Migrations/0003-AddEventRegistrationsTable.cs @@ -0,0 +1,71 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TescEvents.Migrations +{ + public partial class AddEventRegistrationsTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Students", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Students", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "EventRegistrations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + StudentId = table.Column(type: "uuid", nullable: false), + EventId = table.Column(type: "uuid", nullable: false), + UserStatus = table.Column(type: "text", nullable: false), + IsResumeSanitized = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EventRegistrations", x => x.Id); + table.ForeignKey( + name: "FK_EventRegistrations_Events_EventId", + column: x => x.EventId, + principalTable: "Events", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EventRegistrations_Students_StudentId", + column: x => x.StudentId, + principalTable: "Students", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_EventRegistrations_EventId", + table: "EventRegistrations", + column: "EventId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_EventRegistrations_StudentId", + table: "EventRegistrations", + column: "StudentId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EventRegistrations"); + + migrationBuilder.DropTable( + name: "Students"); + } + } +} diff --git a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs index e627166..c91dc91 100644 --- a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs +++ b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs @@ -66,7 +66,66 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Events"); + b.ToTable("Events", (string)null); + }); + + modelBuilder.Entity("TescEvents.Models.EventRegistrations", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("IsResumeSanitized") + .HasColumnType("boolean"); + + b.Property("StudentId") + .HasColumnType("uuid"); + + b.Property("UserStatus") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("StudentId"); + + b.ToTable("EventRegistrations", (string)null); + }); + + modelBuilder.Entity("TescEvents.Models.Student", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Students", (string)null); + }); + + modelBuilder.Entity("TescEvents.Models.EventRegistrations", b => + { + b.HasOne("TescEvents.Models.Event", "Event") + .WithMany() + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TescEvents.Models.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + + b.Navigation("Student"); }); #pragma warning restore 612, 618 } diff --git a/TescEvents/Models/EventRegistrations.cs b/TescEvents/Models/EventRegistrations.cs new file mode 100644 index 0000000..27579a9 --- /dev/null +++ b/TescEvents/Models/EventRegistrations.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace TescEvents.Models; + +[Table("EventRegistrations")] +[Index(nameof(EventId), IsUnique = true)] +public class EventRegistrations { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + public Guid Id { get; set; } + + [ForeignKey(nameof(Student))] + public Guid StudentId { get; set; } + public Student Student { get; set; } + + [ForeignKey(nameof(Event))] + public Guid EventId { get; set; } + public Event Event { get; set; } // Navigation property + + public string UserStatus { get; set; } = UserStatuses.PENDING; + + public bool IsResumeSanitized { get; set; } = false; +} + +public class UserStatuses { + public const string PENDING = "PENDING"; + public const string ACCEPTED = "ACCEPTED"; + public const string REJECTED = "REJECTED"; + + public const string COMMITTED = "COMMITTED"; + public const string DECLINED = "DECLINED"; +} \ No newline at end of file diff --git a/TescEvents/Models/Student.cs b/TescEvents/Models/Student.cs new file mode 100644 index 0000000..7766104 --- /dev/null +++ b/TescEvents/Models/Student.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace TescEvents.Models; + +[Table("Students")] +public class Student { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + public Guid Id { get; set; } +} \ No newline at end of file From 9c7e950eeae6fe72b84dc00da8fe0d0f9a85a2ba Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Sun, 23 Oct 2022 00:34:27 -0700 Subject: [PATCH 18/24] Rename Users to Students --- TescEvents/Controllers/AuthController.cs | 14 +++++------ TescEvents/Controllers/UsersController.cs | 8 +++--- TescEvents/Entities/RepositoryContext.cs | 2 +- TescEvents/Models/{User.cs => Student.cs} | 22 +++++++++++++--- TescEvents/Program.cs | 6 ++--- ...serRepository.cs => IStudentRepository.cs} | 10 ++++---- TescEvents/Repositories/StudentRepository.cs | 25 +++++++++++++++++++ TescEvents/Repositories/UserRepository.cs | 25 ------------------- TescEvents/Utilities/Profiles/UserProfile.cs | 4 +-- TescEvents/Validators/UserValidator.cs | 6 ++--- 10 files changed, 69 insertions(+), 53 deletions(-) rename TescEvents/Models/{User.cs => Student.cs} (64%) rename TescEvents/Repositories/{IUserRepository.cs => IStudentRepository.cs} (70%) create mode 100644 TescEvents/Repositories/StudentRepository.cs delete mode 100644 TescEvents/Repositories/UserRepository.cs diff --git a/TescEvents/Controllers/AuthController.cs b/TescEvents/Controllers/AuthController.cs index 0903572..18e04b1 100644 --- a/TescEvents/Controllers/AuthController.cs +++ b/TescEvents/Controllers/AuthController.cs @@ -20,12 +20,12 @@ namespace TescEvents.Controllers; [ApiController] [Route("/api/[controller]")] public class AuthController : ControllerBase { - private readonly IUserRepository userRepository; - private readonly IValidator userValidator; + private readonly IStudentRepository studentRepository; + private readonly IValidator userValidator; private readonly IMapper mapper; - public AuthController(IUserRepository userRepository, IMapper mapper, IValidator userValidator) { - this.userRepository = userRepository; + public AuthController(IStudentRepository studentRepository, IMapper mapper, IValidator userValidator) { + this.studentRepository = studentRepository; this.mapper = mapper; this.userValidator = userValidator; } @@ -33,7 +33,7 @@ public AuthController(IUserRepository userRepository, IMapper mapper, IValidator [AllowAnonymous] [HttpPost("register", Name = nameof(RegisterUser))] public async Task RegisterUser([Required] [FromForm] UserCreateRequestDTO userReq) { - var userEntity = mapper.Map(userReq); + var userEntity = mapper.Map(userReq); var validationResult = await userValidator.ValidateAsync(userEntity); if (!validationResult.IsValid) return BadRequest( @@ -43,7 +43,7 @@ public async Task RegisterUser([Required] [FromForm] UserCreateRe var salt = GenerateSalt(); userEntity.Salt = salt; userEntity.PasswordHash = HashPassword(userReq.Password, salt); - userRepository.CreateUser(userEntity); + studentRepository.CreateUser(userEntity); var userResponse = mapper.Map(userEntity); return CreatedAtRoute(new { @@ -57,7 +57,7 @@ public async Task RegisterUser([Required] [FromForm] UserCreateRe [AllowAnonymous] [HttpPost(Name = nameof(AuthenticateUser))] public async Task AuthenticateUser([FromForm] string username, [FromForm] string password) { - var user = userRepository.GetUserByUsername(username); + var user = studentRepository.GetUserByUsername(username); if (user == null) return NotFound(); if (HashPassword(password, user.Salt) != user.PasswordHash) return Unauthorized(); diff --git a/TescEvents/Controllers/UsersController.cs b/TescEvents/Controllers/UsersController.cs index 4fedaed..c4f5ec7 100644 --- a/TescEvents/Controllers/UsersController.cs +++ b/TescEvents/Controllers/UsersController.cs @@ -9,11 +9,11 @@ namespace TescEvents.Controllers; [ApiController] [Route("/api/[controller]")] public class UsersController : ControllerBase { - private readonly IUserRepository userRepository; + private readonly IStudentRepository studentRepository; private readonly IMapper mapper; - public UsersController(IUserRepository userRepository, IMapper mapper) { - this.userRepository = userRepository; + public UsersController(IStudentRepository studentRepository, IMapper mapper) { + this.studentRepository = studentRepository; this.mapper = mapper; } @@ -21,7 +21,7 @@ public UsersController(IUserRepository userRepository, IMapper mapper) { [HttpGet(Name = nameof(GetUser))] [Route("user/{uuid}")] public async Task GetUser(string uuid) { - var user = userRepository.GetUserByUuid(Guid.Parse(uuid)); + var user = studentRepository.GetUserByUuid(Guid.Parse(uuid)); if (user == null) return NotFound(); var userResponse = mapper.Map(user); diff --git a/TescEvents/Entities/RepositoryContext.cs b/TescEvents/Entities/RepositoryContext.cs index 546765a..3f9db73 100644 --- a/TescEvents/Entities/RepositoryContext.cs +++ b/TescEvents/Entities/RepositoryContext.cs @@ -12,5 +12,5 @@ protected override void OnConfiguring(DbContextOptionsBuilder options) { } public DbSet? Events { get; set; } - public DbSet? Users { get; set; } + public DbSet? Students { get; set; } } \ No newline at end of file diff --git a/TescEvents/Models/User.cs b/TescEvents/Models/Student.cs similarity index 64% rename from TescEvents/Models/User.cs rename to TescEvents/Models/Student.cs index 39c8625..599977b 100644 --- a/TescEvents/Models/User.cs +++ b/TescEvents/Models/Student.cs @@ -4,9 +4,9 @@ namespace TescEvents.Models; -[Table("Users")] +[Table("Students")] [Index(nameof(Username), IsUnique = true)] -public class User { +public class Student { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Key] public Guid Id { get; set; } @@ -28,10 +28,26 @@ public class User { [Required] public string UserType { get; set; } = UserTypes.REGULAR; + + public string Year { get; set; } + + public string University { get; set; } + + public string Phone { get; set; } + + public string GPA { get; set; } + + public string PID { get; set; } + + public string Gender { get; set; } + + public string Pronouns { get; set; } + + public string Ethnicity { get; set; } } public class UserTypes { public const string REGULAR = "REGULAR"; public const string ADMIN = "ADMIN"; - public const string COORDINATOR = "COORDINATOR"; + public const string ORGANIZER = "ORGANIZER"; } \ No newline at end of file diff --git a/TescEvents/Program.cs b/TescEvents/Program.cs index d5b0b7e..5318097 100644 --- a/TescEvents/Program.cs +++ b/TescEvents/Program.cs @@ -25,11 +25,11 @@ options.UseNpgsql(AppSettings.ConnectionString)); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); // Add validators builder.Services.AddScoped, EventValidator>(); -builder.Services.AddScoped, UserValidator>(); +builder.Services.AddScoped, UserValidator>(); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); @@ -89,7 +89,7 @@ void SeedDb() { End = new DateTime(2022, 9, 23, 14, 0, 0).ToUniversalTime() }); - context.Users.AddRange(new User { + context.Students.AddRange(new Student { Id = Guid.NewGuid(), Username = "sek007@ucsd.edu", FirstName = "Shane", diff --git a/TescEvents/Repositories/IUserRepository.cs b/TescEvents/Repositories/IStudentRepository.cs similarity index 70% rename from TescEvents/Repositories/IUserRepository.cs rename to TescEvents/Repositories/IStudentRepository.cs index cbf598f..398d62a 100644 --- a/TescEvents/Repositories/IUserRepository.cs +++ b/TescEvents/Repositories/IStudentRepository.cs @@ -2,24 +2,24 @@ namespace TescEvents.Repositories; -public interface IUserRepository : IRepositoryBase { +public interface IStudentRepository : IRepositoryBase { /// /// Returns the user entity matching the username, or null if not found /// /// Username of user /// User matching username, or null if not found - User? GetUserByUsername(string username); + Student? GetUserByUsername(string username); /// /// Returns the user entity matching the uuid, or null if not found /// /// /// User matching uuid, or null if not found - User? GetUserByUuid(Guid uuid); + Student? GetUserByUuid(Guid uuid); /// /// Inserts a User entity in the database - /// User to be created + /// User to be created /// - void CreateUser(User user); + void CreateUser(Student student); } \ No newline at end of file diff --git a/TescEvents/Repositories/StudentRepository.cs b/TescEvents/Repositories/StudentRepository.cs new file mode 100644 index 0000000..0deed43 --- /dev/null +++ b/TescEvents/Repositories/StudentRepository.cs @@ -0,0 +1,25 @@ +using TescEvents.Entities; +using TescEvents.Models; + +namespace TescEvents.Repositories; + +public class StudentRepository : RepositoryBase, IStudentRepository { + public StudentRepository(RepositoryContext context) : base(context) { + } + + public Student? GetUserByUsername(string username) { + return FindByCondition(user => user.Username == username) + .FirstOrDefault(); + } + + public Student? GetUserByUuid(Guid uuid) { + return FindByCondition(user => user.Id == uuid) + .FirstOrDefault(); + } + + public void CreateUser(Student student) { + Create(student); + Save(); + } + +} \ No newline at end of file diff --git a/TescEvents/Repositories/UserRepository.cs b/TescEvents/Repositories/UserRepository.cs deleted file mode 100644 index ea5b9fb..0000000 --- a/TescEvents/Repositories/UserRepository.cs +++ /dev/null @@ -1,25 +0,0 @@ -using TescEvents.Entities; -using TescEvents.Models; - -namespace TescEvents.Repositories; - -public class UserRepository : RepositoryBase, IUserRepository { - public UserRepository(RepositoryContext context) : base(context) { - } - - public User? GetUserByUsername(string username) { - return FindByCondition(user => user.Username == username) - .FirstOrDefault(); - } - - public User? GetUserByUuid(Guid uuid) { - return FindByCondition(user => user.Id == uuid) - .FirstOrDefault(); - } - - public void CreateUser(User user) { - Create(user); - Save(); - } - -} \ No newline at end of file diff --git a/TescEvents/Utilities/Profiles/UserProfile.cs b/TescEvents/Utilities/Profiles/UserProfile.cs index de51e40..b439a29 100644 --- a/TescEvents/Utilities/Profiles/UserProfile.cs +++ b/TescEvents/Utilities/Profiles/UserProfile.cs @@ -7,10 +7,10 @@ namespace TescEvents.Utilities.Profiles; public class UserProfile : Profile { public UserProfile() { - CreateMap() + CreateMap() .ForMember(u => u.PasswordHash, option => option.Ignore()) .ForMember(u => u.Salt, option => option.Ignore()); - CreateMap(); + CreateMap(); } } \ No newline at end of file diff --git a/TescEvents/Validators/UserValidator.cs b/TescEvents/Validators/UserValidator.cs index 88cc83d..32e0421 100644 --- a/TescEvents/Validators/UserValidator.cs +++ b/TescEvents/Validators/UserValidator.cs @@ -4,11 +4,11 @@ namespace TescEvents.Validators; -public class UserValidator : AbstractValidator { - public UserValidator(IUserRepository userRepository) { +public class UserValidator : AbstractValidator { + public UserValidator(IStudentRepository studentRepository) { RuleFor(u => u.Username) .EmailAddress() - .Must(u => userRepository.GetUserByUsername(u) == null) + .Must(u => studentRepository.GetUserByUsername(u) == null) .WithMessage("User already exists with that username"); } } \ No newline at end of file From 71d9731262bef6e38ec15489cdeb018ad6ad7f13 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Sun, 23 Oct 2022 00:47:16 -0700 Subject: [PATCH 19/24] Fix migrations --- .../0001-AddEventsTable.Designer.cs | 4 +- TescEvents/Migrations/0001-AddEventsTable.cs | 2 +- .../Migrations/0002-AddUsersTable.Designer.cs | 99 ------------------- TescEvents/Migrations/0002-AddUsersTable.cs | 36 ------- ...UsersToStudentsAndUseVarChars.Designer.cs} | 66 +++++++++++-- ...002-RenameUsersToStudentsAndUseVarChars.cs | 90 +++++++++++++++++ .../Migrations/0003-IndexUserByUsername.cs | 25 ----- .../RepositoryContextModelSnapshot.cs | 62 ++++++++++-- TescEvents/Models/Event.cs | 3 + TescEvents/Models/Student.cs | 12 +++ 10 files changed, 218 insertions(+), 181 deletions(-) delete mode 100644 TescEvents/Migrations/0002-AddUsersTable.Designer.cs delete mode 100644 TescEvents/Migrations/0002-AddUsersTable.cs rename TescEvents/Migrations/{0003-IndexUserByUsername.Designer.cs => 0002-RenameUsersToStudentsAndUseVarChars.Designer.cs} (53%) create mode 100644 TescEvents/Migrations/0002-RenameUsersToStudentsAndUseVarChars.cs delete mode 100644 TescEvents/Migrations/0003-IndexUserByUsername.cs diff --git a/TescEvents/Migrations/0001-AddEventsTable.Designer.cs b/TescEvents/Migrations/0001-AddEventsTable.Designer.cs index 49dc51d..004d8be 100644 --- a/TescEvents/Migrations/0001-AddEventsTable.Designer.cs +++ b/TescEvents/Migrations/0001-AddEventsTable.Designer.cs @@ -12,8 +12,8 @@ namespace TescEvents.Migrations { [DbContext(typeof(RepositoryContext))] - [Migration("20221007021603_0001-AddEventsTable")] - partial class _0001AddEventsTable + [Migration("0001-AddEventsTable")] + partial class AddEventsTable { protected override void BuildTargetModel(ModelBuilder modelBuilder) { diff --git a/TescEvents/Migrations/0001-AddEventsTable.cs b/TescEvents/Migrations/0001-AddEventsTable.cs index 2ab3408..2c975bc 100644 --- a/TescEvents/Migrations/0001-AddEventsTable.cs +++ b/TescEvents/Migrations/0001-AddEventsTable.cs @@ -5,7 +5,7 @@ namespace TescEvents.Migrations { - public partial class _0001AddEventsTable : Migration + public partial class AddEventsTable : Migration { protected override void Up(MigrationBuilder migrationBuilder) { diff --git a/TescEvents/Migrations/0002-AddUsersTable.Designer.cs b/TescEvents/Migrations/0002-AddUsersTable.Designer.cs deleted file mode 100644 index a7c48b8..0000000 --- a/TescEvents/Migrations/0002-AddUsersTable.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TescEvents.Entities; - -#nullable disable - -namespace TescEvents.Migrations -{ - [DbContext(typeof(RepositoryContext))] - [Migration("20221013062041_0002-AddUsersTable")] - partial class _0002AddUsersTable - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "6.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TescEvents.Models.Event", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Archived") - .HasColumnType("boolean"); - - b.Property("Cover") - .HasColumnType("text"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("End") - .HasColumnType("timestamp with time zone"); - - b.Property("Start") - .HasColumnType("timestamp with time zone"); - - b.Property("Thumbnail") - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.ToTable("Events"); - }); - - modelBuilder.Entity("TescEvents.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("Salt") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserType") - .IsRequired() - .HasColumnType("text"); - - b.Property("Username") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/TescEvents/Migrations/0002-AddUsersTable.cs b/TescEvents/Migrations/0002-AddUsersTable.cs deleted file mode 100644 index 83bca8f..0000000 --- a/TescEvents/Migrations/0002-AddUsersTable.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TescEvents.Migrations -{ - public partial class _0002AddUsersTable : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Username = table.Column(type: "text", nullable: false), - FirstName = table.Column(type: "text", nullable: false), - LastName = table.Column(type: "text", nullable: false), - PasswordHash = table.Column(type: "text", nullable: false), - Salt = table.Column(type: "text", nullable: false), - UserType = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Users"); - } - } -} diff --git a/TescEvents/Migrations/0003-IndexUserByUsername.Designer.cs b/TescEvents/Migrations/0002-RenameUsersToStudentsAndUseVarChars.Designer.cs similarity index 53% rename from TescEvents/Migrations/0003-IndexUserByUsername.Designer.cs rename to TescEvents/Migrations/0002-RenameUsersToStudentsAndUseVarChars.Designer.cs index f94788d..feca5a4 100644 --- a/TescEvents/Migrations/0003-IndexUserByUsername.Designer.cs +++ b/TescEvents/Migrations/0002-RenameUsersToStudentsAndUseVarChars.Designer.cs @@ -12,8 +12,8 @@ namespace TescEvents.Migrations { [DbContext(typeof(RepositoryContext))] - [Migration("20221014033420_0003-IndexUserByUsername")] - partial class _0003IndexUserByUsername + [Migration("0004-RenameUsersToStudentsAndUseVarChars")] + partial class RenameUsersToStudentsAndUseVarChars { protected override void BuildTargetModel(ModelBuilder modelBuilder) { @@ -34,7 +34,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("boolean"); b.Property("Cover") - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("Description") .IsRequired() @@ -47,7 +48,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("Thumbnail") - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("Title") .IsRequired() @@ -59,42 +61,86 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Events"); }); - modelBuilder.Entity("TescEvents.Models.User", b => + modelBuilder.Entity("TescEvents.Models.Student", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("Ethnicity") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("FirstName") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("GPA") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("LastName") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PID") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("PasswordHash") .IsRequired() .HasColumnType("text"); + b.Property("Phone") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Pronouns") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("Salt") .IsRequired() .HasColumnType("text"); + b.Property("University") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("UserType") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("Username") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Year") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.HasKey("Id"); b.HasIndex("Username") .IsUnique(); - b.ToTable("Users"); + b.ToTable("Students"); }); #pragma warning restore 612, 618 } diff --git a/TescEvents/Migrations/0002-RenameUsersToStudentsAndUseVarChars.cs b/TescEvents/Migrations/0002-RenameUsersToStudentsAndUseVarChars.cs new file mode 100644 index 0000000..164a82a --- /dev/null +++ b/TescEvents/Migrations/0002-RenameUsersToStudentsAndUseVarChars.cs @@ -0,0 +1,90 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TescEvents.Migrations +{ + public partial class RenameUsersToStudentsAndUseVarChars : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Thumbnail", + table: "Events", + type: "character varying(255)", + maxLength: 255, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Cover", + table: "Events", + type: "character varying(255)", + maxLength: 255, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "Students", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Username = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + FirstName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + LastName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + Salt = table.Column(type: "text", nullable: false), + UserType = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Year = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + University = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Phone = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + GPA = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + PID = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Gender = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Pronouns = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Ethnicity = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Students", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Students_Username", + table: "Students", + column: "Username", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Students"); + + migrationBuilder.AlterColumn( + name: "Thumbnail", + table: "Events", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(255)", + oldMaxLength: 255, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Cover", + table: "Events", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(255)", + oldMaxLength: 255, + oldNullable: true); + } + } +} diff --git a/TescEvents/Migrations/0003-IndexUserByUsername.cs b/TescEvents/Migrations/0003-IndexUserByUsername.cs deleted file mode 100644 index 4d1ab04..0000000 --- a/TescEvents/Migrations/0003-IndexUserByUsername.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TescEvents.Migrations -{ - public partial class _0003IndexUserByUsername : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateIndex( - name: "IX_Users_Username", - table: "Users", - column: "Username", - unique: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_Users_Username", - table: "Users"); - } - } -} diff --git a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs index 49df32a..58304fc 100644 --- a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs +++ b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs @@ -32,7 +32,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean"); b.Property("Cover") - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("Description") .IsRequired() @@ -45,7 +46,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("Thumbnail") - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("Title") .IsRequired() @@ -57,42 +59,86 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Events"); }); - modelBuilder.Entity("TescEvents.Models.User", b => + modelBuilder.Entity("TescEvents.Models.Student", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("Ethnicity") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("FirstName") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("GPA") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("LastName") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PID") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("PasswordHash") .IsRequired() .HasColumnType("text"); + b.Property("Phone") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Pronouns") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("Salt") .IsRequired() .HasColumnType("text"); + b.Property("University") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("UserType") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.Property("Username") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Year") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); b.HasKey("Id"); b.HasIndex("Username") .IsUnique(); - b.ToTable("Users"); + b.ToTable("Students"); }); #pragma warning restore 612, 618 } diff --git a/TescEvents/Models/Event.cs b/TescEvents/Models/Event.cs index 61d1ae0..e3aad86 100644 --- a/TescEvents/Models/Event.cs +++ b/TescEvents/Models/Event.cs @@ -16,8 +16,11 @@ public class Event { [Column(TypeName = "text")] public string Description { get; set; } = ""; + + [MaxLength(255)] public string? Thumbnail { get; set; } + [MaxLength(255)] public string? Cover { get; set; } [Required] diff --git a/TescEvents/Models/Student.cs b/TescEvents/Models/Student.cs index 599977b..ff15bc6 100644 --- a/TescEvents/Models/Student.cs +++ b/TescEvents/Models/Student.cs @@ -12,12 +12,15 @@ public class Student { public Guid Id { get; set; } [Required] + [MaxLength(255)] public string Username { get; set; } [Required] + [MaxLength(255)] public string FirstName { get; set; } [Required] + [MaxLength(255)] public string LastName { get; set; } [Required] @@ -27,22 +30,31 @@ public class Student { public string Salt { get; set; } [Required] + [MaxLength(255)] public string UserType { get; set; } = UserTypes.REGULAR; + [MaxLength(255)] public string Year { get; set; } + [MaxLength(255)] public string University { get; set; } + [MaxLength(255)] public string Phone { get; set; } + [MaxLength(255)] public string GPA { get; set; } + [MaxLength(255)] public string PID { get; set; } + [MaxLength(255)] public string Gender { get; set; } + [MaxLength(255)] public string Pronouns { get; set; } + [MaxLength(255)] public string Ethnicity { get; set; } } From 7fde108fb45682fea319e9de973921d01788358e Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Sun, 23 Oct 2022 23:17:29 -0700 Subject: [PATCH 20/24] Add tests for registering students --- TescEvents.sln.DotSettings.user | 2 +- TescEvents/Controllers/AuthController.cs | 8 +- TescEvents/Program.cs | 2 +- .../{UserValidator.cs => StudentValidator.cs} | 4 +- Tests/AuthControllerTest.cs | 75 +++++++++++++++++++ Tests/Tests.csproj | 4 + 6 files changed, 87 insertions(+), 8 deletions(-) rename TescEvents/Validators/{UserValidator.cs => StudentValidator.cs} (71%) create mode 100644 Tests/AuthControllerTest.cs diff --git a/TescEvents.sln.DotSettings.user b/TescEvents.sln.DotSettings.user index 8acf47f..6d96731 100644 --- a/TescEvents.sln.DotSettings.user +++ b/TescEvents.sln.DotSettings.user @@ -1,6 +1,6 @@  <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <TestAncestor> - <TestId>xUnit::31A35706-A546-4343-B1AB-D43522668AF5::net6.0::Tests.UnitTest1</TestId> + <TestId>xUnit::31A35706-A546-4343-B1AB-D43522668AF5::net6.0::Tests.AuthControllerTest</TestId> </TestAncestor> </SessionState> \ No newline at end of file diff --git a/TescEvents/Controllers/AuthController.cs b/TescEvents/Controllers/AuthController.cs index 18e04b1..b512ff8 100644 --- a/TescEvents/Controllers/AuthController.cs +++ b/TescEvents/Controllers/AuthController.cs @@ -21,13 +21,13 @@ namespace TescEvents.Controllers; [Route("/api/[controller]")] public class AuthController : ControllerBase { private readonly IStudentRepository studentRepository; - private readonly IValidator userValidator; + private readonly IValidator studentValidator; private readonly IMapper mapper; - public AuthController(IStudentRepository studentRepository, IMapper mapper, IValidator userValidator) { + public AuthController(IStudentRepository studentRepository, IMapper mapper, IValidator studentValidator) { this.studentRepository = studentRepository; this.mapper = mapper; - this.userValidator = userValidator; + this.studentValidator = studentValidator; } [AllowAnonymous] @@ -35,7 +35,7 @@ public AuthController(IStudentRepository studentRepository, IMapper mapper, IVal public async Task RegisterUser([Required] [FromForm] UserCreateRequestDTO userReq) { var userEntity = mapper.Map(userReq); - var validationResult = await userValidator.ValidateAsync(userEntity); + var validationResult = await studentValidator.ValidateAsync(userEntity); if (!validationResult.IsValid) return BadRequest( validationResult.Errors .Select(error => error.ErrorMessage)); diff --git a/TescEvents/Program.cs b/TescEvents/Program.cs index 5318097..dad47a9 100644 --- a/TescEvents/Program.cs +++ b/TescEvents/Program.cs @@ -29,7 +29,7 @@ // Add validators builder.Services.AddScoped, EventValidator>(); -builder.Services.AddScoped, UserValidator>(); +builder.Services.AddScoped, StudentValidator>(); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); diff --git a/TescEvents/Validators/UserValidator.cs b/TescEvents/Validators/StudentValidator.cs similarity index 71% rename from TescEvents/Validators/UserValidator.cs rename to TescEvents/Validators/StudentValidator.cs index 32e0421..c8568be 100644 --- a/TescEvents/Validators/UserValidator.cs +++ b/TescEvents/Validators/StudentValidator.cs @@ -4,8 +4,8 @@ namespace TescEvents.Validators; -public class UserValidator : AbstractValidator { - public UserValidator(IStudentRepository studentRepository) { +public class StudentValidator : AbstractValidator { + public StudentValidator(IStudentRepository studentRepository) { RuleFor(u => u.Username) .EmailAddress() .Must(u => studentRepository.GetUserByUsername(u) == null) diff --git a/Tests/AuthControllerTest.cs b/Tests/AuthControllerTest.cs new file mode 100644 index 0000000..7e17308 --- /dev/null +++ b/Tests/AuthControllerTest.cs @@ -0,0 +1,75 @@ +using AutoMapper; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Moq; +using TescEvents.Controllers; +using TescEvents.DTOs.Users; +using TescEvents.Models; +using TescEvents.Repositories; +using TescEvents.Utilities.Profiles; +using TescEvents.Validators; + +namespace Tests; + +public class AuthControllerTest { + private readonly Mock mockStudentRepo; + private readonly IMapper mapper; + private readonly StudentValidator studentValidator; + private readonly AuthController controller; + + public AuthControllerTest() { + mapper = new MapperConfiguration(c => { + c.AddProfile(); + }).CreateMapper(); + + mockStudentRepo = new Mock(); + studentValidator = new StudentValidator(mockStudentRepo.Object); + controller = new AuthController(mockStudentRepo.Object, mapper, studentValidator); + } + + [Fact] + public async Task TestRegisterStudentMinimumInformation() { + // Arrange + var user = new UserCreateRequestDTO { + FirstName = "Shane", + LastName = "Kim", + Password = "supersecretpassword", + Username = "sek007@ucsd.edu" + }; + + // Act + var result = await controller.RegisterUser(user); + + // Assert + Assert.IsType(result); + } + + [Fact] + public async Task TestRegisterStudentDuplicateUsername() { + // Arrange + var user1 = new UserCreateRequestDTO { + FirstName = "Shane", + LastName = "Kim", + Password = "supersecretpassword", + Username = "sek007@ucsd.edu" + }; + var user2 = new UserCreateRequestDTO { + FirstName = "Shane", + LastName = "Kim", + Password = "supersecretpassword", + Username = "sek008@ucsd.edu" + }; + + mockStudentRepo.Setup( + repo => repo.GetUserByUsername(It.Is(u => u == user1.Username))) + .Returns(new Student()); + + // Act + var result1 = await controller.RegisterUser(user1); + var result2 = await controller.RegisterUser(user2); + + // Assert + Assert.IsType(result1); + Assert.IsType(result2); + } +} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 534b5cc..3a2e003 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -22,4 +22,8 @@ + + + + From 1bf9f152cd33a14a95de22e0a48c68639067fe78 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Mon, 24 Oct 2022 09:33:56 -0700 Subject: [PATCH 21/24] Add auth service interface scaffold --- TescEvents/Controllers/EventsController.cs | 1 + TescEvents/Services/IAuthService.cs | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 TescEvents/Services/IAuthService.cs diff --git a/TescEvents/Controllers/EventsController.cs b/TescEvents/Controllers/EventsController.cs index fd14445..7051542 100644 --- a/TescEvents/Controllers/EventsController.cs +++ b/TescEvents/Controllers/EventsController.cs @@ -37,6 +37,7 @@ public IActionResult GetEvents(string? start = "", string? end = "") { && e.End <= endFilter.ToUniversalTime())); } + [Authorize] [HttpPost(Name = nameof(CreateEvent))] public async Task CreateEvent([Required] [FromForm] EventCreateRequestDTO e) { var eventEntity = mapper.Map(e); diff --git a/TescEvents/Services/IAuthService.cs b/TescEvents/Services/IAuthService.cs new file mode 100644 index 0000000..0eb2b58 --- /dev/null +++ b/TescEvents/Services/IAuthService.cs @@ -0,0 +1,5 @@ +namespace TescEvents.Services; + +public interface IAuthService { + +} \ No newline at end of file From eb1490c2d6b19c4759ded4bc0073432e88dea20a Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Mon, 24 Oct 2022 18:19:30 -0700 Subject: [PATCH 22/24] Register for events --- TescEvents/Controllers/AuthController.cs | 4 +- TescEvents/Controllers/EventsController.cs | 31 +++++++++++++- TescEvents/Entities/RepositoryContext.cs | 2 +- ...tRegistrations.cs => EventRegistration.cs} | 4 +- TescEvents/Models/Student.cs | 40 ++++++++----------- TescEvents/Program.cs | 6 ++- .../EventRegistrationRepository.cs | 20 ++++++++++ TescEvents/Repositories/EventRepository.cs | 4 ++ .../IEventRegistrationRepository.cs | 7 ++++ TescEvents/Repositories/IEventRepository.cs | 2 +- TescEvents/Repositories/IRepositoryBase.cs | 5 +++ TescEvents/Services/IAuthService.cs | 4 +- 12 files changed, 96 insertions(+), 33 deletions(-) rename TescEvents/Models/{EventRegistrations.cs => EventRegistration.cs} (90%) create mode 100644 TescEvents/Repositories/EventRegistrationRepository.cs create mode 100644 TescEvents/Repositories/IEventRegistrationRepository.cs diff --git a/TescEvents/Controllers/AuthController.cs b/TescEvents/Controllers/AuthController.cs index b512ff8..f0a9ac2 100644 --- a/TescEvents/Controllers/AuthController.cs +++ b/TescEvents/Controllers/AuthController.cs @@ -67,8 +67,8 @@ public async Task AuthenticateUser([FromForm] string username, [F var key = Encoding.ASCII.GetBytes(Environment.GetEnvironmentVariable("JWT_KEY")!); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { - new Claim(JwtRegisteredClaimNames.Sub, user.Username), - new Claim(JwtRegisteredClaimNames.Jti, new Guid().ToString()), + new Claim(ClaimTypes.Actor, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.Username), }), Expires = DateTime.UtcNow.AddMinutes(AppSettings.VALID_JWT_LENGTH_DAYS), Issuer = issuer, diff --git a/TescEvents/Controllers/EventsController.cs b/TescEvents/Controllers/EventsController.cs index 7051542..89c1930 100644 --- a/TescEvents/Controllers/EventsController.cs +++ b/TescEvents/Controllers/EventsController.cs @@ -1,12 +1,15 @@ using System.ComponentModel.DataAnnotations; +using System.Security.Claims; using AutoMapper; using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using TescEvents.DTOs; using TescEvents.DTOs.Events; using TescEvents.Models; using TescEvents.Repositories; +using TescEvents.Services; namespace TescEvents.Controllers; @@ -14,13 +17,20 @@ namespace TescEvents.Controllers; [Route("/api/[controller]")] public class EventsController : ControllerBase { private readonly IEventRepository eventRepository; + private readonly IEventRegistrationRepository registrationRepository; + private readonly IStudentRepository studentRepository; private readonly IMapper mapper; private readonly IValidator validator; - public EventsController(IEventRepository eventRepository, IMapper mapper, IValidator validator) { + public EventsController(IEventRepository eventRepository, + IEventRegistrationRepository registrationRepository, + IMapper mapper, + IValidator validator, IStudentRepository studentRepository) { this.eventRepository = eventRepository; + this.registrationRepository = registrationRepository; this.mapper = mapper; this.validator = validator; + this.studentRepository = studentRepository; } [HttpGet(Name = nameof(GetEvents))] @@ -54,4 +64,23 @@ public async Task CreateEvent([Required] [FromForm] EventCreateRe var eventResponse = mapper.Map(eventEntity); return CreatedAtRoute(nameof(CreateEvent), new { Id = eventResponse.Id }, eventResponse); } + + [Authorize] + [HttpPost("event/{eventId}/register", Name = nameof(RegisterForEvent))] + public async Task RegisterForEvent(string eventId) { + var _event = eventRepository.FindByCondition(e => e.Id == Guid.Parse(eventId)) + .AsNoTracking() + .FirstOrDefault(); + if (_event == null) return NotFound(); + + var studentId = HttpContext.User.FindFirstValue(ClaimTypes.Actor); + if (studentId == null) return Unauthorized(); + var student = studentRepository.GetUserByUuid(Guid.Parse(studentId)); + if (student == null) return Unauthorized(); + + registrationRepository.RegisterStudentForEvent(student, _event); + // TODO: Send registration email confirmations + + return Ok(); + } } \ No newline at end of file diff --git a/TescEvents/Entities/RepositoryContext.cs b/TescEvents/Entities/RepositoryContext.cs index a1d416f..05ad9e5 100644 --- a/TescEvents/Entities/RepositoryContext.cs +++ b/TescEvents/Entities/RepositoryContext.cs @@ -12,6 +12,6 @@ protected override void OnConfiguring(DbContextOptionsBuilder options) { } public DbSet? Events { get; set; } - public DbSet? EventRegistrations { get; set; } + public DbSet? EventRegistrations { get; set; } public DbSet? Students { get; set; } } \ No newline at end of file diff --git a/TescEvents/Models/EventRegistrations.cs b/TescEvents/Models/EventRegistration.cs similarity index 90% rename from TescEvents/Models/EventRegistrations.cs rename to TescEvents/Models/EventRegistration.cs index 27579a9..b424209 100644 --- a/TescEvents/Models/EventRegistrations.cs +++ b/TescEvents/Models/EventRegistration.cs @@ -6,7 +6,7 @@ namespace TescEvents.Models; [Table("EventRegistrations")] [Index(nameof(EventId), IsUnique = true)] -public class EventRegistrations { +public class EventRegistration { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Key] public Guid Id { get; set; } @@ -17,7 +17,7 @@ public class EventRegistrations { [ForeignKey(nameof(Event))] public Guid EventId { get; set; } - public Event Event { get; set; } // Navigation property + public Event? Event { get; set; } // Navigation property public string UserStatus { get; set; } = UserStatuses.PENDING; diff --git a/TescEvents/Models/Student.cs b/TescEvents/Models/Student.cs index ff15bc6..2d092db 100644 --- a/TescEvents/Models/Student.cs +++ b/TescEvents/Models/Student.cs @@ -32,30 +32,22 @@ public class Student { [Required] [MaxLength(255)] public string UserType { get; set; } = UserTypes.REGULAR; - - [MaxLength(255)] - public string Year { get; set; } - - [MaxLength(255)] - public string University { get; set; } - - [MaxLength(255)] - public string Phone { get; set; } - - [MaxLength(255)] - public string GPA { get; set; } - - [MaxLength(255)] - public string PID { get; set; } - - [MaxLength(255)] - public string Gender { get; set; } - - [MaxLength(255)] - public string Pronouns { get; set; } - - [MaxLength(255)] - public string Ethnicity { get; set; } + + [MaxLength(255)] public string Year { get; set; } = ""; + + [MaxLength(255)] public string University { get; set; } = ""; + + [MaxLength(255)] public string Phone { get; set; } = ""; + + [MaxLength(255)] public string GPA { get; set; } = ""; + + [MaxLength(255)] public string PID { get; set; } = ""; + + [MaxLength(255)] public string Gender { get; set; } = ""; + + [MaxLength(255)] public string Pronouns { get; set; } = ""; + + [MaxLength(255)] public string Ethnicity { get; set; } = ""; } public class UserTypes { diff --git a/TescEvents/Program.cs b/TescEvents/Program.cs index dad47a9..d56af15 100644 --- a/TescEvents/Program.cs +++ b/TescEvents/Program.cs @@ -1,11 +1,13 @@ using System.Text; using FluentValidation; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using TescEvents.Entities; using TescEvents.Models; using TescEvents.Repositories; +using TescEvents.Services; using TescEvents.Utilities; using TescEvents.Utilities.Profiles; using TescEvents.Validators; @@ -26,6 +28,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Add validators builder.Services.AddScoped, EventValidator>(); @@ -53,6 +56,7 @@ ValidateIssuerSigningKey = true, }; }); +builder.Services.AddAuthorization(); var app = builder.Build(); @@ -60,8 +64,8 @@ app.UseHttpsRedirection(); -app.UseAuthorization(); app.UseAuthentication(); +app.UseAuthorization(); app.MapControllers(); diff --git a/TescEvents/Repositories/EventRegistrationRepository.cs b/TescEvents/Repositories/EventRegistrationRepository.cs new file mode 100644 index 0000000..e97240c --- /dev/null +++ b/TescEvents/Repositories/EventRegistrationRepository.cs @@ -0,0 +1,20 @@ +using TescEvents.Entities; +using TescEvents.Models; + +namespace TescEvents.Repositories; + +public class EventRegistrationRepository : RepositoryBase, IEventRegistrationRepository { + public EventRegistrationRepository(RepositoryContext context) : base(context) { + } + + public void RegisterStudentForEvent(Student student, Event e) { + Create(new EventRegistration { + Student = student, + StudentId = student.Id, + Event = e, + EventId = e.Id, + UserStatus = UserStatuses.PENDING, + IsResumeSanitized = false, + }); + } +} \ No newline at end of file diff --git a/TescEvents/Repositories/EventRepository.cs b/TescEvents/Repositories/EventRepository.cs index 04c1250..795a636 100644 --- a/TescEvents/Repositories/EventRepository.cs +++ b/TescEvents/Repositories/EventRepository.cs @@ -6,4 +6,8 @@ namespace TescEvents.Repositories; public class EventRepository : RepositoryBase, IEventRepository { public EventRepository(RepositoryContext context) : base(context) { } + + public Event? GetEventByUuid(Guid eventId) { + return FindByCondition(e => e.Id == eventId).FirstOrDefault(); + } } \ No newline at end of file diff --git a/TescEvents/Repositories/IEventRegistrationRepository.cs b/TescEvents/Repositories/IEventRegistrationRepository.cs new file mode 100644 index 0000000..9c9286f --- /dev/null +++ b/TescEvents/Repositories/IEventRegistrationRepository.cs @@ -0,0 +1,7 @@ +using TescEvents.Models; + +namespace TescEvents.Repositories; + +public interface IEventRegistrationRepository : IRepositoryBase { + void RegisterStudentForEvent(Student student, Event e); +} \ No newline at end of file diff --git a/TescEvents/Repositories/IEventRepository.cs b/TescEvents/Repositories/IEventRepository.cs index e9504fb..cad1abf 100644 --- a/TescEvents/Repositories/IEventRepository.cs +++ b/TescEvents/Repositories/IEventRepository.cs @@ -3,5 +3,5 @@ namespace TescEvents.Repositories; public interface IEventRepository : IRepositoryBase { - + Event? GetEventByUuid(Guid eventId); } \ No newline at end of file diff --git a/TescEvents/Repositories/IRepositoryBase.cs b/TescEvents/Repositories/IRepositoryBase.cs index 6a0fd95..4a2f488 100644 --- a/TescEvents/Repositories/IRepositoryBase.cs +++ b/TescEvents/Repositories/IRepositoryBase.cs @@ -4,6 +4,11 @@ namespace TescEvents.Repositories; public interface IRepositoryBase { IQueryable FindAll(); + /// + /// Returns the model in the database context matching the specified predicate, or null if not found + /// + /// + /// IQueryable FindByCondition(Expression> expression); void Create(T entity); void Update(T entity); diff --git a/TescEvents/Services/IAuthService.cs b/TescEvents/Services/IAuthService.cs index 0eb2b58..ad392eb 100644 --- a/TescEvents/Services/IAuthService.cs +++ b/TescEvents/Services/IAuthService.cs @@ -1,5 +1,7 @@ +using TescEvents.Models; + namespace TescEvents.Services; public interface IAuthService { - + Student? GetStudentFromClaim(string jwt); } \ No newline at end of file From 490bf9d598b098ee4b209452e2e8cad2b9277c2a Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Mon, 24 Oct 2022 21:26:54 -0700 Subject: [PATCH 23/24] Reset migrations --- TescEvents/Controllers/EventsController.cs | 1 - .../0001-AddEventsTable.Designer.cs | 64 --------- TescEvents/Migrations/0001-AddEventsTable.cs | 33 ----- ...r.cs => 0001-InitialMigration.Designer.cs} | 64 ++++++++- .../Migrations/0001-InitialMigration.cs | 19 +++ .../0002-AddApplicationColumns.Designer.cs | 76 ---------- .../Migrations/0002-AddApplicationColumns.cs | 58 -------- ...003-RenameUsersToStudentsAndUseVarChars.cs | 90 ------------ ...004-AddEventRegistrationsTable.Designer.cs | 135 ------------------ .../0004-AddEventRegistrationsTable.cs | 56 -------- .../RepositoryContextModelSnapshot.cs | 55 +++---- TescEvents/Models/EventRegistration.cs | 8 +- .../EventRegistrationRepository.cs | 3 +- 13 files changed, 109 insertions(+), 553 deletions(-) delete mode 100644 TescEvents/Migrations/0001-AddEventsTable.Designer.cs delete mode 100644 TescEvents/Migrations/0001-AddEventsTable.cs rename TescEvents/Migrations/{0003-RenameUsersToStudentsAndUseVarChars.Designer.cs => 0001-InitialMigration.Designer.cs} (70%) create mode 100644 TescEvents/Migrations/0001-InitialMigration.cs delete mode 100644 TescEvents/Migrations/0002-AddApplicationColumns.Designer.cs delete mode 100644 TescEvents/Migrations/0002-AddApplicationColumns.cs delete mode 100644 TescEvents/Migrations/0003-RenameUsersToStudentsAndUseVarChars.cs delete mode 100644 TescEvents/Migrations/0004-AddEventRegistrationsTable.Designer.cs delete mode 100644 TescEvents/Migrations/0004-AddEventRegistrationsTable.cs diff --git a/TescEvents/Controllers/EventsController.cs b/TescEvents/Controllers/EventsController.cs index 89c1930..3417445 100644 --- a/TescEvents/Controllers/EventsController.cs +++ b/TescEvents/Controllers/EventsController.cs @@ -69,7 +69,6 @@ public async Task CreateEvent([Required] [FromForm] EventCreateRe [HttpPost("event/{eventId}/register", Name = nameof(RegisterForEvent))] public async Task RegisterForEvent(string eventId) { var _event = eventRepository.FindByCondition(e => e.Id == Guid.Parse(eventId)) - .AsNoTracking() .FirstOrDefault(); if (_event == null) return NotFound(); diff --git a/TescEvents/Migrations/0001-AddEventsTable.Designer.cs b/TescEvents/Migrations/0001-AddEventsTable.Designer.cs deleted file mode 100644 index 004d8be..0000000 --- a/TescEvents/Migrations/0001-AddEventsTable.Designer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TescEvents.Entities; - -#nullable disable - -namespace TescEvents.Migrations -{ - [DbContext(typeof(RepositoryContext))] - [Migration("0001-AddEventsTable")] - partial class AddEventsTable - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "6.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TescEvents.Models.Event", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Archived") - .HasColumnType("boolean"); - - b.Property("Cover") - .HasColumnType("text"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("End") - .HasColumnType("timestamp with time zone"); - - b.Property("Start") - .HasColumnType("timestamp with time zone"); - - b.Property("Thumbnail") - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.ToTable("Events"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/TescEvents/Migrations/0001-AddEventsTable.cs b/TescEvents/Migrations/0001-AddEventsTable.cs deleted file mode 100644 index 7cd5c51..0000000 --- a/TescEvents/Migrations/0001-AddEventsTable.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TescEvents.Migrations -{ - public partial class AddEventsTable : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.CreateTable( - name: "Events", - columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "character varying(255)", - maxLength: 255, nullable: false), - Description = table.Column(type: "text", nullable: false), - Thumbnail = table.Column(type: "text", nullable: true), - Cover = table.Column(type: "text", nullable: true), - Start = table.Column(type: "timestamp with time zone", - nullable: false), - End = table.Column(type: "timestamp with time zone", - nullable: false), - Archived = table.Column(type: "boolean", nullable: false) - }, - constraints: table => { table.PrimaryKey("PK_Events", x => x.Id); }); - } - - protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "Events"); - } - } -} diff --git a/TescEvents/Migrations/0003-RenameUsersToStudentsAndUseVarChars.Designer.cs b/TescEvents/Migrations/0001-InitialMigration.Designer.cs similarity index 70% rename from TescEvents/Migrations/0003-RenameUsersToStudentsAndUseVarChars.Designer.cs rename to TescEvents/Migrations/0001-InitialMigration.Designer.cs index a8bf01d..543850c 100644 --- a/TescEvents/Migrations/0003-RenameUsersToStudentsAndUseVarChars.Designer.cs +++ b/TescEvents/Migrations/0001-InitialMigration.Designer.cs @@ -12,8 +12,8 @@ namespace TescEvents.Migrations { [DbContext(typeof(RepositoryContext))] - [Migration("0003-RenameUsersToStudentsAndUseVarChars")] - partial class RenameUsersToStudentsAndUseVarChars + [Migration("0001-InitialMigration")] + partial class InitialMigration { protected override void BuildTargetModel(ModelBuilder modelBuilder) { @@ -30,6 +30,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AcceptingApplications") + .HasColumnType("boolean"); + + b.Property("ApplicationCloseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApplicationOpenDate") + .HasColumnType("timestamp with time zone"); + b.Property("Archived") .HasColumnType("boolean"); @@ -44,6 +53,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("End") .HasColumnType("timestamp with time zone"); + b.Property("RequiresApplication") + .HasColumnType("boolean"); + b.Property("Start") .HasColumnType("timestamp with time zone"); @@ -61,6 +73,35 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Events"); }); + modelBuilder.Entity("TescEvents.Models.EventRegistration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("IsResumeSanitized") + .HasColumnType("boolean"); + + b.Property("StudentId") + .HasColumnType("uuid"); + + b.Property("UserStatus") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("StudentId"); + + b.ToTable("EventRegistrations"); + }); + modelBuilder.Entity("TescEvents.Models.Student", b => { b.Property("Id") @@ -142,6 +183,25 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Students"); }); + + modelBuilder.Entity("TescEvents.Models.EventRegistration", b => + { + b.HasOne("TescEvents.Models.Event", "Event") + .WithMany() + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TescEvents.Models.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + + b.Navigation("Student"); + }); #pragma warning restore 612, 618 } } diff --git a/TescEvents/Migrations/0001-InitialMigration.cs b/TescEvents/Migrations/0001-InitialMigration.cs new file mode 100644 index 0000000..d09f3b3 --- /dev/null +++ b/TescEvents/Migrations/0001-InitialMigration.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TescEvents.Migrations +{ + public partial class InitialMigration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/TescEvents/Migrations/0002-AddApplicationColumns.Designer.cs b/TescEvents/Migrations/0002-AddApplicationColumns.Designer.cs deleted file mode 100644 index 5f9cc91..0000000 --- a/TescEvents/Migrations/0002-AddApplicationColumns.Designer.cs +++ /dev/null @@ -1,76 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TescEvents.Entities; - -#nullable disable - -namespace TescEvents.Migrations -{ - [DbContext(typeof(RepositoryContext))] - [Migration("0002-AddApplicationColumns")] - partial class AddApplicationColumns - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "6.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TescEvents.Models.Event", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AcceptingApplications") - .HasColumnType("boolean"); - - b.Property("ApplicationCloseDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ApplicationOpenDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Archived") - .HasColumnType("boolean"); - - b.Property("Cover") - .HasColumnType("text"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("End") - .HasColumnType("timestamp with time zone"); - - b.Property("RequiresApplication") - .HasColumnType("boolean"); - - b.Property("Start") - .HasColumnType("timestamp with time zone"); - - b.Property("Thumbnail") - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.ToTable("Events"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/TescEvents/Migrations/0002-AddApplicationColumns.cs b/TescEvents/Migrations/0002-AddApplicationColumns.cs deleted file mode 100644 index efb7e7a..0000000 --- a/TescEvents/Migrations/0002-AddApplicationColumns.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TescEvents.Migrations -{ - public partial class AddApplicationColumns : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AcceptingApplications", - table: "Events", - type: "boolean", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "ApplicationCloseDate", - table: "Events", - type: "timestamp with time zone", - nullable: true); - - migrationBuilder.AddColumn( - name: "ApplicationOpenDate", - table: "Events", - type: "timestamp with time zone", - nullable: true); - - migrationBuilder.AddColumn( - name: "RequiresApplication", - table: "Events", - type: "boolean", - nullable: false, - defaultValue: false); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "AcceptingApplications", - table: "Events"); - - migrationBuilder.DropColumn( - name: "ApplicationCloseDate", - table: "Events"); - - migrationBuilder.DropColumn( - name: "ApplicationOpenDate", - table: "Events"); - - migrationBuilder.DropColumn( - name: "RequiresApplication", - table: "Events"); - } - } -} diff --git a/TescEvents/Migrations/0003-RenameUsersToStudentsAndUseVarChars.cs b/TescEvents/Migrations/0003-RenameUsersToStudentsAndUseVarChars.cs deleted file mode 100644 index 164a82a..0000000 --- a/TescEvents/Migrations/0003-RenameUsersToStudentsAndUseVarChars.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TescEvents.Migrations -{ - public partial class RenameUsersToStudentsAndUseVarChars : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Thumbnail", - table: "Events", - type: "character varying(255)", - maxLength: 255, - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Cover", - table: "Events", - type: "character varying(255)", - maxLength: 255, - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); - - migrationBuilder.CreateTable( - name: "Students", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Username = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - FirstName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - LastName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - PasswordHash = table.Column(type: "text", nullable: false), - Salt = table.Column(type: "text", nullable: false), - UserType = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - Year = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - University = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - Phone = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - GPA = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - PID = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - Gender = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - Pronouns = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - Ethnicity = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Students", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_Students_Username", - table: "Students", - column: "Username", - unique: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Students"); - - migrationBuilder.AlterColumn( - name: "Thumbnail", - table: "Events", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(255)", - oldMaxLength: 255, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Cover", - table: "Events", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "character varying(255)", - oldMaxLength: 255, - oldNullable: true); - } - } -} diff --git a/TescEvents/Migrations/0004-AddEventRegistrationsTable.Designer.cs b/TescEvents/Migrations/0004-AddEventRegistrationsTable.Designer.cs deleted file mode 100644 index 3778501..0000000 --- a/TescEvents/Migrations/0004-AddEventRegistrationsTable.Designer.cs +++ /dev/null @@ -1,135 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TescEvents.Entities; - -#nullable disable - -namespace TescEvents.Migrations -{ - [DbContext(typeof(RepositoryContext))] - [Migration("0004-AddEventRegistrationsTable")] - partial class AddEventRegistrationsTable - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "6.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TescEvents.Models.Event", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AcceptingApplications") - .HasColumnType("boolean"); - - b.Property("ApplicationCloseDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ApplicationOpenDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Archived") - .HasColumnType("boolean"); - - b.Property("Cover") - .HasColumnType("text"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("End") - .HasColumnType("timestamp with time zone"); - - b.Property("RequiresApplication") - .HasColumnType("boolean"); - - b.Property("Start") - .HasColumnType("timestamp with time zone"); - - b.Property("Thumbnail") - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.ToTable("Events"); - }); - - modelBuilder.Entity("TescEvents.Models.EventRegistrations", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EventId") - .HasColumnType("uuid"); - - b.Property("IsResumeSanitized") - .HasColumnType("boolean"); - - b.Property("StudentId") - .HasColumnType("uuid"); - - b.Property("UserStatus") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("EventId") - .IsUnique(); - - b.HasIndex("StudentId"); - - b.ToTable("EventRegistrations"); - }); - - modelBuilder.Entity("TescEvents.Models.Student", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("Students"); - }); - - modelBuilder.Entity("TescEvents.Models.EventRegistrations", b => - { - b.HasOne("TescEvents.Models.Event", "Event") - .WithMany() - .HasForeignKey("EventId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("TescEvents.Models.Student", "Student") - .WithMany() - .HasForeignKey("StudentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Event"); - - b.Navigation("Student"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/TescEvents/Migrations/0004-AddEventRegistrationsTable.cs b/TescEvents/Migrations/0004-AddEventRegistrationsTable.cs deleted file mode 100644 index bf67dae..0000000 --- a/TescEvents/Migrations/0004-AddEventRegistrationsTable.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TescEvents.Migrations -{ - public partial class AddEventRegistrationsTable : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "EventRegistrations", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - StudentId = table.Column(type: "uuid", nullable: false), - EventId = table.Column(type: "uuid", nullable: false), - UserStatus = table.Column(type: "text", nullable: false), - IsResumeSanitized = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_EventRegistrations", x => x.Id); - table.ForeignKey( - name: "FK_EventRegistrations_Events_EventId", - column: x => x.EventId, - principalTable: "Events", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_EventRegistrations_Students_StudentId", - column: x => x.StudentId, - principalTable: "Students", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_EventRegistrations_EventId", - table: "EventRegistrations", - column: "EventId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_EventRegistrations_StudentId", - table: "EventRegistrations", - column: "StudentId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "EventRegistrations"); - } - } -} diff --git a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs index cde5ec4..1cd3edd 100644 --- a/TescEvents/Migrations/RepositoryContextModelSnapshot.cs +++ b/TescEvents/Migrations/RepositoryContextModelSnapshot.cs @@ -68,10 +68,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Events", (string)null); + b.ToTable("Events"); }); - modelBuilder.Entity("TescEvents.Models.EventRegistrations", b => + modelBuilder.Entity("TescEvents.Models.EventRegistration", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -97,37 +97,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("StudentId"); - b.ToTable("EventRegistrations", (string)null); - }); - - modelBuilder.Entity("TescEvents.Models.Student", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("Students", (string)null); - }); - - modelBuilder.Entity("TescEvents.Models.EventRegistrations", b => - { - b.HasOne("TescEvents.Models.Event", "Event") - .WithMany() - .HasForeignKey("EventId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("TescEvents.Models.Student", "Student") - .WithMany() - .HasForeignKey("StudentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Event"); - - b.Navigation("Student"); + b.ToTable("EventRegistrations"); }); modelBuilder.Entity("TescEvents.Models.Student", b => @@ -211,6 +181,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Students"); }); + + modelBuilder.Entity("TescEvents.Models.EventRegistration", b => + { + b.HasOne("TescEvents.Models.Event", "Event") + .WithMany() + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TescEvents.Models.Student", "Student") + .WithMany() + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + + b.Navigation("Student"); + }); #pragma warning restore 612, 618 } } diff --git a/TescEvents/Models/EventRegistration.cs b/TescEvents/Models/EventRegistration.cs index b424209..b2344ad 100644 --- a/TescEvents/Models/EventRegistration.cs +++ b/TescEvents/Models/EventRegistration.cs @@ -11,13 +11,15 @@ public class EventRegistration { [Key] public Guid Id { get; set; } - [ForeignKey(nameof(Student))] public Guid StudentId { get; set; } + + [ForeignKey(nameof(StudentId))] public Student Student { get; set; } - [ForeignKey(nameof(Event))] public Guid EventId { get; set; } - public Event? Event { get; set; } // Navigation property + + [ForeignKey(nameof(EventId))] + public Event Event { get; set; } // Navigation property public string UserStatus { get; set; } = UserStatuses.PENDING; diff --git a/TescEvents/Repositories/EventRegistrationRepository.cs b/TescEvents/Repositories/EventRegistrationRepository.cs index e97240c..2fa1793 100644 --- a/TescEvents/Repositories/EventRegistrationRepository.cs +++ b/TescEvents/Repositories/EventRegistrationRepository.cs @@ -9,12 +9,11 @@ public EventRegistrationRepository(RepositoryContext context) : base(context) { public void RegisterStudentForEvent(Student student, Event e) { Create(new EventRegistration { - Student = student, StudentId = student.Id, - Event = e, EventId = e.Id, UserStatus = UserStatuses.PENDING, IsResumeSanitized = false, }); + Save(); } } \ No newline at end of file From 8ddd3907685e59ecea8f0e286b641df2bbd6fd41 Mon Sep 17 00:00:00 2001 From: Shane Kim Date: Tue, 1 Nov 2022 17:32:52 -0700 Subject: [PATCH 24/24] Test event validation behaviour and scaffold business logic --- TescEvents.sln.DotSettings.user | 7 +- TescEvents/Controllers/EventsController.cs | 21 ++-- TescEvents/Entities/RepositoryContext.cs | 10 +- TescEvents/Repositories/EventRepository.cs | 5 + TescEvents/Repositories/IEventRepository.cs | 13 +++ TescEvents/Services/IEmailService.cs | 15 +++ TescEvents/Services/IUploadService.cs | 5 + Tests/Tests.csproj | 5 + Tests/{ => UnitTests}/AuthControllerTest.cs | 3 +- Tests/UnitTests/EventTest.cs | 120 ++++++++++++++++++++ Tests/UnitTests/EventValidatorTest.cs | 95 ++++++++++++++++ Tests/UnitTests/EventsControllerTest.cs | 59 ++++++++++ 12 files changed, 344 insertions(+), 14 deletions(-) create mode 100644 TescEvents/Services/IEmailService.cs create mode 100644 TescEvents/Services/IUploadService.cs rename Tests/{ => UnitTests}/AuthControllerTest.cs (98%) create mode 100644 Tests/UnitTests/EventTest.cs create mode 100644 Tests/UnitTests/EventValidatorTest.cs create mode 100644 Tests/UnitTests/EventsControllerTest.cs diff --git a/TescEvents.sln.DotSettings.user b/TescEvents.sln.DotSettings.user index 6d96731..57c85d8 100644 --- a/TescEvents.sln.DotSettings.user +++ b/TescEvents.sln.DotSettings.user @@ -1,6 +1,11 @@  <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <TestAncestor> - <TestId>xUnit::31A35706-A546-4343-B1AB-D43522668AF5::net6.0::Tests.AuthControllerTest</TestId> + <TestId>xUnit::31A35706-A546-4343-B1AB-D43522668AF5::net6.0::Tests.UnitTests.EventTest</TestId> + <TestId>xUnit::31A35706-A546-4343-B1AB-D43522668AF5::net6.0::Tests.UnitTests.EventValidatorTest.TestEventBasic</TestId> + <TestId>xUnit::31A35706-A546-4343-B1AB-D43522668AF5::net6.0::Tests.UnitTests.EventValidatorTest.TestEndIsBeforeOrEqualToStart</TestId> + <TestId>xUnit::31A35706-A546-4343-B1AB-D43522668AF5::net6.0::Tests.UnitTests.EventValidatorTest.TestEventTitleEmptyOrNull</TestId> + <TestId>xUnit::31A35706-A546-4343-B1AB-D43522668AF5::net6.0::Tests.UnitTests.EventValidatorTest.TestEventDescriptionEmptyOrNull</TestId> + <TestId>xUnit::31A35706-A546-4343-B1AB-D43522668AF5::net6.0::Tests.UnitTests.EventValidatorTest</TestId> </TestAncestor> </SessionState> \ No newline at end of file diff --git a/TescEvents/Controllers/EventsController.cs b/TescEvents/Controllers/EventsController.cs index 3417445..d6d1ffb 100644 --- a/TescEvents/Controllers/EventsController.cs +++ b/TescEvents/Controllers/EventsController.cs @@ -21,16 +21,20 @@ public class EventsController : ControllerBase { private readonly IStudentRepository studentRepository; private readonly IMapper mapper; private readonly IValidator validator; + private readonly IUploadService uploadService; public EventsController(IEventRepository eventRepository, IEventRegistrationRepository registrationRepository, IMapper mapper, - IValidator validator, IStudentRepository studentRepository) { + IValidator validator, + IStudentRepository studentRepository, + IUploadService uploadService) { this.eventRepository = eventRepository; this.registrationRepository = registrationRepository; this.mapper = mapper; this.validator = validator; this.studentRepository = studentRepository; + this.uploadService = uploadService; } [HttpGet(Name = nameof(GetEvents))] @@ -43,8 +47,9 @@ public IActionResult GetEvents(string? start = "", string? end = "") { endFilter = DateTime.MaxValue; } - return Ok(eventRepository.FindByCondition(e => e.Start >= startFilter.ToUniversalTime() - && e.End <= endFilter.ToUniversalTime())); + return Ok(eventRepository + .GetAllEventsWithinRange(startFilter.ToUniversalTime(), + endFilter.ToUniversalTime())); } [Authorize] @@ -56,9 +61,12 @@ public async Task CreateEvent([Required] [FromForm] EventCreateRe if (!validationResult.IsValid) return BadRequest( validationResult.Errors.Select(error => error.ErrorMessage)); - // TODO: Abstract into service transaction and async call eventRepository.Create(eventEntity); - // TODO: Upload image to AWS + // Upload image to AWS + if (e.Thumbnail != null) + uploadService.UploadFileToPath(e.Thumbnail, ""); + if (e.Cover != null) + uploadService.UploadFileToPath(e.Cover, ""); eventRepository.Save(); var eventResponse = mapper.Map(eventEntity); @@ -68,8 +76,7 @@ public async Task CreateEvent([Required] [FromForm] EventCreateRe [Authorize] [HttpPost("event/{eventId}/register", Name = nameof(RegisterForEvent))] public async Task RegisterForEvent(string eventId) { - var _event = eventRepository.FindByCondition(e => e.Id == Guid.Parse(eventId)) - .FirstOrDefault(); + var _event = eventRepository.GetEventByUuid(Guid.Parse(eventId)); if (_event == null) return NotFound(); var studentId = HttpContext.User.FindFirstValue(ClaimTypes.Actor); diff --git a/TescEvents/Entities/RepositoryContext.cs b/TescEvents/Entities/RepositoryContext.cs index 05ad9e5..6e58cde 100644 --- a/TescEvents/Entities/RepositoryContext.cs +++ b/TescEvents/Entities/RepositoryContext.cs @@ -8,10 +8,12 @@ public class RepositoryContext : DbContext { public RepositoryContext(DbContextOptions options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder options) { - options.UseNpgsql(AppSettings.ConnectionString); + if (!options.IsConfigured) { + options.UseNpgsql(AppSettings.ConnectionString); + } } - public DbSet? Events { get; set; } - public DbSet? EventRegistrations { get; set; } - public DbSet? Students { get; set; } + public virtual DbSet? Events { get; set; } + public virtual DbSet? EventRegistrations { get; set; } + public virtual DbSet? Students { get; set; } } \ No newline at end of file diff --git a/TescEvents/Repositories/EventRepository.cs b/TescEvents/Repositories/EventRepository.cs index 795a636..63d5f27 100644 --- a/TescEvents/Repositories/EventRepository.cs +++ b/TescEvents/Repositories/EventRepository.cs @@ -10,4 +10,9 @@ public EventRepository(RepositoryContext context) : base(context) { public Event? GetEventByUuid(Guid eventId) { return FindByCondition(e => e.Id == eventId).FirstOrDefault(); } + + public IEnumerable GetAllEventsWithinRange(DateTime start, DateTime end) { + return FindByCondition(e => e.Start >= start && e.Start <= end) + .OrderBy(e => e.Start); + } } \ No newline at end of file diff --git a/TescEvents/Repositories/IEventRepository.cs b/TescEvents/Repositories/IEventRepository.cs index cad1abf..312d55f 100644 --- a/TescEvents/Repositories/IEventRepository.cs +++ b/TescEvents/Repositories/IEventRepository.cs @@ -3,5 +3,18 @@ namespace TescEvents.Repositories; public interface IEventRepository : IRepositoryBase { + /// + /// Gets an event by UUID, or null if not found + /// + /// + /// Event? GetEventByUuid(Guid eventId); + + /// + /// Returns all events with a start time between start and end + /// + /// + /// + /// + IEnumerable GetAllEventsWithinRange(DateTime start, DateTime end); } \ No newline at end of file diff --git a/TescEvents/Services/IEmailService.cs b/TescEvents/Services/IEmailService.cs new file mode 100644 index 0000000..f16693f --- /dev/null +++ b/TescEvents/Services/IEmailService.cs @@ -0,0 +1,15 @@ +namespace TescEvents.Services; + +public interface IEmailService { + /// + /// Sends an event registration confirmation email to a given email address + /// + /// + void SendEventRegistrationConfirmationEmail(string email); + + /// + /// Sends a signup confirmation email to a given email address + /// + /// + void SendSignupConfirmationEmail(string email); +} \ No newline at end of file diff --git a/TescEvents/Services/IUploadService.cs b/TescEvents/Services/IUploadService.cs new file mode 100644 index 0000000..2e1ec9d --- /dev/null +++ b/TescEvents/Services/IUploadService.cs @@ -0,0 +1,5 @@ +namespace TescEvents.Services; + +public interface IUploadService { + void UploadFileToPath(IFormFile file, string path); +} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 3a2e003..f6a01b2 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -9,6 +9,7 @@ + @@ -26,4 +27,8 @@ + + + + diff --git a/Tests/AuthControllerTest.cs b/Tests/UnitTests/AuthControllerTest.cs similarity index 98% rename from Tests/AuthControllerTest.cs rename to Tests/UnitTests/AuthControllerTest.cs index 7e17308..c980fb1 100644 --- a/Tests/AuthControllerTest.cs +++ b/Tests/UnitTests/AuthControllerTest.cs @@ -1,5 +1,4 @@ using AutoMapper; -using FluentValidation; using Microsoft.AspNetCore.Mvc; using Moq; using TescEvents.Controllers; @@ -9,7 +8,7 @@ using TescEvents.Utilities.Profiles; using TescEvents.Validators; -namespace Tests; +namespace Tests.UnitTests; public class AuthControllerTest { private readonly Mock mockStudentRepo; diff --git a/Tests/UnitTests/EventTest.cs b/Tests/UnitTests/EventTest.cs new file mode 100644 index 0000000..304ae62 --- /dev/null +++ b/Tests/UnitTests/EventTest.cs @@ -0,0 +1,120 @@ +using Microsoft.EntityFrameworkCore; +using Moq; +using TescEvents.Entities; +using TescEvents.Models; +using TescEvents.Repositories; + +namespace Tests.UnitTests; + +public class EventTest : IDisposable { + private readonly RepositoryContext context; + private readonly IEventRepository repo; + + public EventTest() { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "tesc-events") + .Options; + + context = new RepositoryContext(options); + repo = new EventRepository(context); + } + + [Fact] + public void TestGetSingleEvent() { + // Arrange + var guid = Guid.NewGuid(); + var pastEvent = new Event { + Id = guid, + Title = "Past Event", + Start = new DateTime(2021, 10, 12, 10, 0, 0).ToUniversalTime(), + End = new DateTime(2021, 10, 12, 14, 0, 0).ToUniversalTime(), + }; + + context.Events.Add(pastEvent); + context.SaveChanges(); + + // Act + Event? e = repo.GetEventByUuid(guid); + Event? nonexistentEvent = repo.GetEventByUuid(Guid.NewGuid()); + + // Assert + Assert.NotNull(e); + Assert.Equal(guid, e!.Id); + + Assert.Null(nonexistentEvent); + } + + [Fact] + public void TestCreateEvent() { + // Arrange + var guid = Guid.NewGuid(); + var e = new Event { + Id = guid, + Title = "New Event", + Start = DateTime.UtcNow, + End = DateTime.UtcNow.AddDays(1), + }; + + // Act + repo.Create(e); + repo.Save(); + + // Assert + var createdEvent = context.Events.Find(guid); + + Assert.NotNull(createdEvent); + } + + [Fact] + public void TestGetEventsWithinRange() { + // Arrange + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + var guid3 = Guid.NewGuid(); + + var events = new[] { + new Event { + Id = guid1, + Title = "Past Event", + Start = DateTime.UtcNow.AddDays(-20), + End = DateTime.UtcNow.AddDays(-19), + }, + new Event { + Id = guid2, + Title = "Future Event", + Start = DateTime.UtcNow.AddDays(10), + End = DateTime.UtcNow.AddDays(10.5) + }, + new Event { + Id = guid3, + Title = "Current Event", + Start = DateTime.UtcNow.AddDays(-1), + End = DateTime.UtcNow.AddDays(1), + }, + }; + + context.Events.AddRange(events); + context.SaveChanges(); + + // Act + var all = repo.GetAllEventsWithinRange(DateTime.MinValue, DateTime.MaxValue).ToList(); + var pastTilNow = repo.GetAllEventsWithinRange(DateTime.MinValue, DateTime.UtcNow).ToList(); + var future = repo.GetAllEventsWithinRange(DateTime.UtcNow, DateTime.MaxValue).ToList(); + + // Assert + Assert.Collection(all, + e => Assert.Equal(guid1, e.Id), + e => Assert.Equal(guid3, e.Id), + e => Assert.Equal(guid2, e.Id)); + Assert.Collection(pastTilNow, + e => Assert.Equal(guid1, e.Id), + e => Assert.Equal(guid3, e.Id)); + Assert.Collection(future, + e => Assert.Equal(guid2, e.Id)); + } + + public void Dispose() { + context.Database.EnsureDeleted(); + context.Dispose(); + } +} \ No newline at end of file diff --git a/Tests/UnitTests/EventValidatorTest.cs b/Tests/UnitTests/EventValidatorTest.cs new file mode 100644 index 0000000..6a36a44 --- /dev/null +++ b/Tests/UnitTests/EventValidatorTest.cs @@ -0,0 +1,95 @@ +using FluentValidation.TestHelper; +using TescEvents.Models; +using TescEvents.Validators; + +namespace Tests.UnitTests; + +public class EventValidatorTest { + private readonly EventValidator eventValidator; + + public EventValidatorTest() { + eventValidator = new EventValidator(); + } + + [Fact] + public void TestEventBasic() { + // Arrange + var e = new Event { + Title = "New Event", + Description = "Event description", + Start = DateTime.Now.AddDays(1), + End = DateTime.Now.AddDays(1).AddHours(1), + }; + + // Act + var result = eventValidator.TestValidate(e); + + // Assert + result.ShouldNotHaveValidationErrorFor(e => e.Title); + result.ShouldNotHaveValidationErrorFor(e => e.Description); + result.ShouldNotHaveValidationErrorFor(e => e.Start); + result.ShouldNotHaveValidationErrorFor(e => e.End); + } + + private static readonly DateTime eventStart = DateTime.Now.AddHours(1); + public static readonly object[][] EndBeforeStartData = { + new object[] { eventStart, eventStart }, + new object[] { eventStart, eventStart.AddSeconds(-1) }, + new object[] { eventStart, DateTime.UnixEpoch }, + }; + [Theory, MemberData(nameof(EndBeforeStartData))] + public void TestEndIsBeforeOrEqualToStart(DateTime start, DateTime end) { + // Arrange + var e = new Event { + Title = "New Event", + Description = "Event description", + Start = start, + End = end, + }; + + // Act + var result = eventValidator.TestValidate(e); + + // Assert + result.ShouldNotHaveValidationErrorFor(e => e.Start); + result.ShouldHaveValidationErrorFor(e => e.End); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void TestEventTitleEmptyOrNull(string title) { + // Arrange + var e = new Event { + Title = title, + Description = "Event description", + Start = DateTime.Now.AddHours(1), + End = DateTime.Now.AddHours(2), + }; + + // Act + var result = eventValidator.TestValidate(e); + + // Assert + result.ShouldHaveValidationErrorFor(e => e.Title); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void TestEventDescriptionEmptyOrNull(string description) { + // Arrange + var e = new Event { + Title = "New Event", + Description = description, + Start = DateTime.Now.AddHours(1), + End = DateTime.Now.AddHours(2), + }; + + // Act + var result = eventValidator.TestValidate(e); + + // Assert + result.ShouldHaveValidationErrorFor(e => e.Description); + } +} \ No newline at end of file diff --git a/Tests/UnitTests/EventsControllerTest.cs b/Tests/UnitTests/EventsControllerTest.cs new file mode 100644 index 0000000..e362c87 --- /dev/null +++ b/Tests/UnitTests/EventsControllerTest.cs @@ -0,0 +1,59 @@ +using AutoMapper; +using Moq; +using TescEvents.Controllers; +using TescEvents.Repositories; +using TescEvents.Services; +using TescEvents.Utilities.Profiles; +using TescEvents.Validators; + +namespace Tests.UnitTests; + +public class EventsControllerTest { + private readonly Mock mockEventRepo; + private readonly Mock mockRegistrationRepo; + private readonly Mock mockStudentRepo; + private readonly Mock mockUploadService; + private readonly EventValidator eventValidator; + private readonly IMapper mapper; + + private readonly EventsController controller; + + public EventsControllerTest() { + mapper = new MapperConfiguration(c => { + c.AddProfile(); + }).CreateMapper(); + + mockEventRepo = new Mock(); + mockRegistrationRepo = new Mock(); + mockUploadService = new Mock(); + mockStudentRepo = new Mock(); + eventValidator = new EventValidator(); + + controller = new EventsController(mockEventRepo.Object, + mockRegistrationRepo.Object, + mapper, + eventValidator, + mockStudentRepo.Object, + mockUploadService.Object); + } + + [Fact] + public async Task TestGetEventsDefaultShowsAllFuture() { + // Arrange + + // Act + + + // Assert + } + + [Fact] + public async Task TestGetEventsFromStartDate() { + + } + + [Fact] + public async Task TestGetEventsBeforeEndDate() { + + } +} \ No newline at end of file