diff --git a/SurveyBackend/SurveyBackend.API/Controllers/AuthController.cs b/SurveyBackend/SurveyBackend.API/Controllers/AuthController.cs index 93fb89a..882c94b 100644 --- a/SurveyBackend/SurveyBackend.API/Controllers/AuthController.cs +++ b/SurveyBackend/SurveyBackend.API/Controllers/AuthController.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc; +using SurveyBackend.Core.Services; using SurveyBackend.DTOs; +using SurveyBackend.Mappers.UserDTOs; namespace SurveyBackend.Controllers; @@ -7,9 +9,24 @@ namespace SurveyBackend.Controllers; [Route("auth")] public class AuthController : ControllerBase { - [HttpPost("login")] - public async Task GetToken([FromBody] UserLoginDto loginData) + private readonly IAuthorizationService _authorizationService; + + public AuthController(IAuthorizationService authorizationService) { - return Ok(); + _authorizationService = authorizationService; + } + + [HttpPost("login")] + public async Task LogIn([FromBody] UserLoginDto loginData) + { + var token = await _authorizationService.LogInUser(loginData.Email, loginData.Password); + return Ok(new { token = token }); + } + + [HttpPost("register")] + public async Task Register([FromBody] UserRegistrationDto registerData) + { + var token = await _authorizationService.RegisterUser(UserRegistrationMapper.UserRegistrationToModel(registerData)); + return Ok(new { token = token }); } } \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.API/Mappers/UserDTOs/UserRegistrationMapper.cs b/SurveyBackend/SurveyBackend.API/Mappers/UserDTOs/UserRegistrationMapper.cs new file mode 100644 index 0000000..5d98322 --- /dev/null +++ b/SurveyBackend/SurveyBackend.API/Mappers/UserDTOs/UserRegistrationMapper.cs @@ -0,0 +1,16 @@ +using SurveyBackend.Core.Models; +using SurveyBackend.Core.Services; +using SurveyBackend.DTOs; + +namespace SurveyBackend.Mappers.UserDTOs; + +public static class UserRegistrationMapper +{ + public static User UserRegistrationToModel(UserRegistrationDto dto) => new User + { + Email = dto.Email, + FirstName = dto.FirstName, + LastName = dto.LastName, + Password = dto.Password, + }; +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.API/Middlewares/ExceptionsMiddleware.cs b/SurveyBackend/SurveyBackend.API/Middlewares/ExceptionsMiddleware.cs new file mode 100644 index 0000000..65c94f8 --- /dev/null +++ b/SurveyBackend/SurveyBackend.API/Middlewares/ExceptionsMiddleware.cs @@ -0,0 +1,49 @@ +using SurveyBackend.Services.Exceptions; + +namespace SurveyBackend.Middlewares; + +public class ExceptionsMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionsMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (ServiceException ex) + { + context.Response.StatusCode = ex.StatusCode; + context.Response.ContentType = "application/json"; + + var response = new + { + error = ex.Message + }; + + await context.Response.WriteAsJsonAsync(response); + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + + context.Response.StatusCode = 500; + context.Response.ContentType = "application/json"; + + var response = new + { + error = "Internal Server Error. GG WP, request bub fix" + }; + + await context.Response.WriteAsJsonAsync(response); + } + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.API/Program.cs b/SurveyBackend/SurveyBackend.API/Program.cs index 2aa90b2..6a2bf3f 100644 --- a/SurveyBackend/SurveyBackend.API/Program.cs +++ b/SurveyBackend/SurveyBackend.API/Program.cs @@ -2,8 +2,14 @@ using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; +using SurveyBackend.Core.Repositories; +using SurveyBackend.Core.Services; using SurveyBackend.Infrastructure; using SurveyBackend.Infrastructure.Data; +using SurveyBackend.Infrastructure.Repositories; +using SurveyBackend.Middlewares; +using SurveyBackend.Services; +using SurveyBackend.Services.Services; using SurveyLib.Core.Repositories; using SurveyLib.Core.Services; using SurveyLib.Infrastructure.EFCore.Data; @@ -27,6 +33,13 @@ public class Program builder.Services.AddScoped(provider => provider.GetRequiredService()); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -59,6 +72,8 @@ public class Program app.UseSwaggerUI(); } + app.UseMiddleware(); + app.UseAuthentication(); app.UseAuthorization(); diff --git a/SurveyBackend/SurveyBackend.API/SurveyBackend.API.csproj b/SurveyBackend/SurveyBackend.API/SurveyBackend.API.csproj index 3764109..b736412 100644 --- a/SurveyBackend/SurveyBackend.API/SurveyBackend.API.csproj +++ b/SurveyBackend/SurveyBackend.API/SurveyBackend.API.csproj @@ -10,13 +10,18 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/SurveyBackend/SurveyBackend.Core/Models/User.cs b/SurveyBackend/SurveyBackend.Core/Models/User.cs index 78f2286..257d4fa 100644 --- a/SurveyBackend/SurveyBackend.Core/Models/User.cs +++ b/SurveyBackend/SurveyBackend.Core/Models/User.cs @@ -4,7 +4,7 @@ namespace SurveyBackend.Core.Models; public class User { - public string Id { get; set; } + public int Id { get; set; } public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } diff --git a/SurveyBackend/SurveyBackend.Core/Repositories/IUserRepository.cs b/SurveyBackend/SurveyBackend.Core/Repositories/IUserRepository.cs index 791059a..311742b 100644 --- a/SurveyBackend/SurveyBackend.Core/Repositories/IUserRepository.cs +++ b/SurveyBackend/SurveyBackend.Core/Repositories/IUserRepository.cs @@ -4,4 +4,5 @@ namespace SurveyBackend.Core.Repositories; public interface IUserRepository : IGenericRepository { + public Task GetUserByEmail(string email); } \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Core/Services/IAuthorizationService.cs b/SurveyBackend/SurveyBackend.Core/Services/IAuthorizationService.cs new file mode 100644 index 0000000..0237a22 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Core/Services/IAuthorizationService.cs @@ -0,0 +1,9 @@ +using SurveyBackend.Core.Models; + +namespace SurveyBackend.Core.Services; + +public interface IAuthorizationService +{ + public Task LogInUser(string email, string password); + public Task RegisterUser(User user); +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Services/IPasswordHasher.cs b/SurveyBackend/SurveyBackend.Core/Services/IPasswordHasher.cs similarity index 74% rename from SurveyBackend/SurveyBackend.Infrastructure/Services/IPasswordHasher.cs rename to SurveyBackend/SurveyBackend.Core/Services/IPasswordHasher.cs index 52d1427..6e2ec29 100644 --- a/SurveyBackend/SurveyBackend.Infrastructure/Services/IPasswordHasher.cs +++ b/SurveyBackend/SurveyBackend.Core/Services/IPasswordHasher.cs @@ -1,4 +1,4 @@ -namespace SurveyBackend.Infrastructure.Services; +namespace SurveyBackend.Core.Services; public interface IPasswordHasher { diff --git a/SurveyBackend/SurveyBackend.Core/Services/IUserService.cs b/SurveyBackend/SurveyBackend.Core/Services/IUserService.cs index 7d97a29..cd34c76 100644 --- a/SurveyBackend/SurveyBackend.Core/Services/IUserService.cs +++ b/SurveyBackend/SurveyBackend.Core/Services/IUserService.cs @@ -1,6 +1,10 @@ +using SurveyBackend.Core.Models; + namespace SurveyBackend.Core.Services; public interface IUserService { - + public Task GetUserByEmail(string email); + public Task IsEmailTaken(string email); + public Task CreateUserAsync(User user); } \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Core/SurveyBackend.Core.csproj b/SurveyBackend/SurveyBackend.Core/SurveyBackend.Core.csproj index adbb79f..8cc2397 100644 --- a/SurveyBackend/SurveyBackend.Core/SurveyBackend.Core.csproj +++ b/SurveyBackend/SurveyBackend.Core/SurveyBackend.Core.csproj @@ -7,7 +7,7 @@ - + diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Data/Migrations/20250418123442_Initial.Designer.cs b/SurveyBackend/SurveyBackend.Infrastructure/Data/Migrations/20250418123442_Initial.Designer.cs new file mode 100644 index 0000000..b8b673d --- /dev/null +++ b/SurveyBackend/SurveyBackend.Infrastructure/Data/Migrations/20250418123442_Initial.Designer.cs @@ -0,0 +1,321 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SurveyBackend.Infrastructure.Data; + +#nullable disable + +namespace SurveyBackend.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250418123442_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.15"); + + modelBuilder.Entity("GroupUser", b => + { + b.Property("GroupsId") + .HasColumnType("INTEGER"); + + b.Property("UsersId") + .HasColumnType("INTEGER"); + + b.HasKey("GroupsId", "UsersId"); + + b.HasIndex("UsersId"); + + b.ToTable("GroupUser"); + }); + + modelBuilder.Entity("SurveyBackend.Core.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Label") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("SurveyBackend.Core.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Answer", b => + { + b.Property("CompletionId") + .HasColumnType("INTEGER"); + + b.Property("QuestionId") + .HasColumnType("INTEGER"); + + b.Property("AnswerText") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("CompletionId", "QuestionId"); + + b.HasIndex("QuestionId"); + + b.ToTable("Answers"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.AnswerVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MultipleAnswerQuestionId") + .HasColumnType("INTEGER"); + + b.Property("QuestionId") + .HasColumnType("INTEGER"); + + b.Property("SingleAnswerQuestionId") + .HasColumnType("INTEGER"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MultipleAnswerQuestionId"); + + b.HasIndex("QuestionId"); + + b.HasIndex("SingleAnswerQuestionId"); + + b.ToTable("AnswerVariant"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Completion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FinishedAt") + .HasColumnType("TEXT"); + + b.Property("SurveyId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SurveyId"); + + b.ToTable("Completions"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("SurveyId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SurveyId"); + + b.ToTable("Questions"); + + b.HasDiscriminator().HasValue("QuestionBase"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Survey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Surveys"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.MultipleAnswerQuestion", b => + { + b.HasBaseType("SurveyLib.Core.Models.QuestionBase"); + + b.HasDiscriminator().HasValue("MultipleAnswerQuestion"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.SingleAnswerQuestion", b => + { + b.HasBaseType("SurveyLib.Core.Models.QuestionBase"); + + b.HasDiscriminator().HasValue("SingleAnswerQuestion"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.TextQuestion", b => + { + b.HasBaseType("SurveyLib.Core.Models.QuestionBase"); + + b.HasDiscriminator().HasValue("TextQuestion"); + }); + + modelBuilder.Entity("GroupUser", b => + { + b.HasOne("SurveyBackend.Core.Models.Group", null) + .WithMany() + .HasForeignKey("GroupsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SurveyBackend.Core.Models.User", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Answer", b => + { + b.HasOne("SurveyLib.Core.Models.Completion", "Completion") + .WithMany("Answers") + .HasForeignKey("CompletionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SurveyLib.Core.Models.QuestionBase", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Completion"); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.AnswerVariant", b => + { + b.HasOne("SurveyLib.Core.Models.QuestionVariants.MultipleAnswerQuestion", null) + .WithMany("AnswerVariants") + .HasForeignKey("MultipleAnswerQuestionId"); + + b.HasOne("SurveyLib.Core.Models.QuestionBase", "Question") + .WithMany() + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SurveyLib.Core.Models.QuestionVariants.SingleAnswerQuestion", null) + .WithMany("AnswerVariants") + .HasForeignKey("SingleAnswerQuestionId"); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Completion", b => + { + b.HasOne("SurveyLib.Core.Models.Survey", "Survey") + .WithMany("Completions") + .HasForeignKey("SurveyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Survey"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b => + { + b.HasOne("SurveyLib.Core.Models.Survey", "Survey") + .WithMany("Questions") + .HasForeignKey("SurveyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Survey"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Completion", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Survey", b => + { + b.Navigation("Completions"); + + b.Navigation("Questions"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.MultipleAnswerQuestion", b => + { + b.Navigation("AnswerVariants"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.SingleAnswerQuestion", b => + { + b.Navigation("AnswerVariants"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Data/Migrations/20250418123442_Initial.cs b/SurveyBackend/SurveyBackend.Infrastructure/Data/Migrations/20250418123442_Initial.cs new file mode 100644 index 0000000..26fbf8b --- /dev/null +++ b/SurveyBackend/SurveyBackend.Infrastructure/Data/Migrations/20250418123442_Initial.cs @@ -0,0 +1,243 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SurveyBackend.Infrastructure.Data.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Groups", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Label = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Groups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Surveys", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Surveys", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Email = table.Column(type: "TEXT", nullable: false), + FirstName = table.Column(type: "TEXT", nullable: false), + LastName = table.Column(type: "TEXT", nullable: false), + Password = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Completions", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SurveyId = table.Column(type: "INTEGER", nullable: false), + FinishedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Completions", x => x.Id); + table.ForeignKey( + name: "FK_Completions_Surveys_SurveyId", + column: x => x.SurveyId, + principalTable: "Surveys", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Questions", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SurveyId = table.Column(type: "INTEGER", nullable: false), + Title = table.Column(type: "TEXT", nullable: false), + Discriminator = table.Column(type: "TEXT", maxLength: 34, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Questions", x => x.Id); + table.ForeignKey( + name: "FK_Questions_Surveys_SurveyId", + column: x => x.SurveyId, + principalTable: "Surveys", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "GroupUser", + columns: table => new + { + GroupsId = table.Column(type: "INTEGER", nullable: false), + UsersId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GroupUser", x => new { x.GroupsId, x.UsersId }); + table.ForeignKey( + name: "FK_GroupUser_Groups_GroupsId", + column: x => x.GroupsId, + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GroupUser_Users_UsersId", + column: x => x.UsersId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Answers", + columns: table => new + { + CompletionId = table.Column(type: "INTEGER", nullable: false), + QuestionId = table.Column(type: "INTEGER", nullable: false), + AnswerText = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Answers", x => new { x.CompletionId, x.QuestionId }); + table.ForeignKey( + name: "FK_Answers_Completions_CompletionId", + column: x => x.CompletionId, + principalTable: "Completions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Answers_Questions_QuestionId", + column: x => x.QuestionId, + principalTable: "Questions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AnswerVariant", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + QuestionId = table.Column(type: "INTEGER", nullable: false), + Text = table.Column(type: "TEXT", nullable: false), + MultipleAnswerQuestionId = table.Column(type: "INTEGER", nullable: true), + SingleAnswerQuestionId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AnswerVariant", x => x.Id); + table.ForeignKey( + name: "FK_AnswerVariant_Questions_MultipleAnswerQuestionId", + column: x => x.MultipleAnswerQuestionId, + principalTable: "Questions", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_AnswerVariant_Questions_QuestionId", + column: x => x.QuestionId, + principalTable: "Questions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AnswerVariant_Questions_SingleAnswerQuestionId", + column: x => x.SingleAnswerQuestionId, + principalTable: "Questions", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Answers_QuestionId", + table: "Answers", + column: "QuestionId"); + + migrationBuilder.CreateIndex( + name: "IX_AnswerVariant_MultipleAnswerQuestionId", + table: "AnswerVariant", + column: "MultipleAnswerQuestionId"); + + migrationBuilder.CreateIndex( + name: "IX_AnswerVariant_QuestionId", + table: "AnswerVariant", + column: "QuestionId"); + + migrationBuilder.CreateIndex( + name: "IX_AnswerVariant_SingleAnswerQuestionId", + table: "AnswerVariant", + column: "SingleAnswerQuestionId"); + + migrationBuilder.CreateIndex( + name: "IX_Completions_SurveyId", + table: "Completions", + column: "SurveyId"); + + migrationBuilder.CreateIndex( + name: "IX_GroupUser_UsersId", + table: "GroupUser", + column: "UsersId"); + + migrationBuilder.CreateIndex( + name: "IX_Questions_SurveyId", + table: "Questions", + column: "SurveyId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Answers"); + + migrationBuilder.DropTable( + name: "AnswerVariant"); + + migrationBuilder.DropTable( + name: "GroupUser"); + + migrationBuilder.DropTable( + name: "Completions"); + + migrationBuilder.DropTable( + name: "Questions"); + + migrationBuilder.DropTable( + name: "Groups"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Surveys"); + } + } +} diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/SurveyBackend/SurveyBackend.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..db4d61e --- /dev/null +++ b/SurveyBackend/SurveyBackend.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,318 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SurveyBackend.Infrastructure.Data; + +#nullable disable + +namespace SurveyBackend.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.15"); + + modelBuilder.Entity("GroupUser", b => + { + b.Property("GroupsId") + .HasColumnType("INTEGER"); + + b.Property("UsersId") + .HasColumnType("INTEGER"); + + b.HasKey("GroupsId", "UsersId"); + + b.HasIndex("UsersId"); + + b.ToTable("GroupUser"); + }); + + modelBuilder.Entity("SurveyBackend.Core.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Label") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("SurveyBackend.Core.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Answer", b => + { + b.Property("CompletionId") + .HasColumnType("INTEGER"); + + b.Property("QuestionId") + .HasColumnType("INTEGER"); + + b.Property("AnswerText") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("CompletionId", "QuestionId"); + + b.HasIndex("QuestionId"); + + b.ToTable("Answers"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.AnswerVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MultipleAnswerQuestionId") + .HasColumnType("INTEGER"); + + b.Property("QuestionId") + .HasColumnType("INTEGER"); + + b.Property("SingleAnswerQuestionId") + .HasColumnType("INTEGER"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MultipleAnswerQuestionId"); + + b.HasIndex("QuestionId"); + + b.HasIndex("SingleAnswerQuestionId"); + + b.ToTable("AnswerVariant"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Completion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FinishedAt") + .HasColumnType("TEXT"); + + b.Property("SurveyId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SurveyId"); + + b.ToTable("Completions"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("SurveyId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SurveyId"); + + b.ToTable("Questions"); + + b.HasDiscriminator().HasValue("QuestionBase"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Survey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Surveys"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.MultipleAnswerQuestion", b => + { + b.HasBaseType("SurveyLib.Core.Models.QuestionBase"); + + b.HasDiscriminator().HasValue("MultipleAnswerQuestion"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.SingleAnswerQuestion", b => + { + b.HasBaseType("SurveyLib.Core.Models.QuestionBase"); + + b.HasDiscriminator().HasValue("SingleAnswerQuestion"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.TextQuestion", b => + { + b.HasBaseType("SurveyLib.Core.Models.QuestionBase"); + + b.HasDiscriminator().HasValue("TextQuestion"); + }); + + modelBuilder.Entity("GroupUser", b => + { + b.HasOne("SurveyBackend.Core.Models.Group", null) + .WithMany() + .HasForeignKey("GroupsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SurveyBackend.Core.Models.User", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Answer", b => + { + b.HasOne("SurveyLib.Core.Models.Completion", "Completion") + .WithMany("Answers") + .HasForeignKey("CompletionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SurveyLib.Core.Models.QuestionBase", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Completion"); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.AnswerVariant", b => + { + b.HasOne("SurveyLib.Core.Models.QuestionVariants.MultipleAnswerQuestion", null) + .WithMany("AnswerVariants") + .HasForeignKey("MultipleAnswerQuestionId"); + + b.HasOne("SurveyLib.Core.Models.QuestionBase", "Question") + .WithMany() + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SurveyLib.Core.Models.QuestionVariants.SingleAnswerQuestion", null) + .WithMany("AnswerVariants") + .HasForeignKey("SingleAnswerQuestionId"); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Completion", b => + { + b.HasOne("SurveyLib.Core.Models.Survey", "Survey") + .WithMany("Completions") + .HasForeignKey("SurveyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Survey"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b => + { + b.HasOne("SurveyLib.Core.Models.Survey", "Survey") + .WithMany("Questions") + .HasForeignKey("SurveyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Survey"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Completion", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.Survey", b => + { + b.Navigation("Completions"); + + b.Navigation("Questions"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.MultipleAnswerQuestion", b => + { + b.Navigation("AnswerVariants"); + }); + + modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.SingleAnswerQuestion", b => + { + b.Navigation("AnswerVariants"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Repositories/UserRepository.cs b/SurveyBackend/SurveyBackend.Infrastructure/Repositories/UserRepository.cs index bd1980d..86124da 100644 --- a/SurveyBackend/SurveyBackend.Infrastructure/Repositories/UserRepository.cs +++ b/SurveyBackend/SurveyBackend.Infrastructure/Repositories/UserRepository.cs @@ -41,4 +41,9 @@ public class UserRepository : IUserRepository _context.Users.Remove(entity); await _context.SaveChangesAsync(); } + + public async Task GetUserByEmail(string email) + { + return await _context.Users.FirstOrDefaultAsync(u => u.Email == email); + } } \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Infrastructure/SurveyBackend.Infrastructure.csproj b/SurveyBackend/SurveyBackend.Infrastructure/SurveyBackend.Infrastructure.csproj index 8d6e23a..e46ce0e 100644 --- a/SurveyBackend/SurveyBackend.Infrastructure/SurveyBackend.Infrastructure.csproj +++ b/SurveyBackend/SurveyBackend.Infrastructure/SurveyBackend.Infrastructure.csproj @@ -7,7 +7,11 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -16,4 +20,8 @@ + + + + diff --git a/SurveyBackend/SurveyBackend.Infrastructure/AuthOptions.cs b/SurveyBackend/SurveyBackend.Services/AuthOptions.cs similarity index 96% rename from SurveyBackend/SurveyBackend.Infrastructure/AuthOptions.cs rename to SurveyBackend/SurveyBackend.Services/AuthOptions.cs index cd379e8..7d92d6f 100644 --- a/SurveyBackend/SurveyBackend.Infrastructure/AuthOptions.cs +++ b/SurveyBackend/SurveyBackend.Services/AuthOptions.cs @@ -2,7 +2,7 @@ using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; -namespace SurveyBackend.Infrastructure; +namespace SurveyBackend.Services; public static class AuthOptions { diff --git a/SurveyBackend/SurveyBackend.Services/Exceptions/ConflictException.cs b/SurveyBackend/SurveyBackend.Services/Exceptions/ConflictException.cs new file mode 100644 index 0000000..12d14ce --- /dev/null +++ b/SurveyBackend/SurveyBackend.Services/Exceptions/ConflictException.cs @@ -0,0 +1,10 @@ +namespace SurveyBackend.Services.Exceptions; + +public class ConflictException : ServiceException +{ + public override int StatusCode => 409; + + public ConflictException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Services/Exceptions/NotFoundException.cs b/SurveyBackend/SurveyBackend.Services/Exceptions/NotFoundException.cs new file mode 100644 index 0000000..a5e5739 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Services/Exceptions/NotFoundException.cs @@ -0,0 +1,10 @@ +namespace SurveyBackend.Services.Exceptions; + +public class NotFoundException : ServiceException +{ + public override int StatusCode => 404; + + public NotFoundException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Services/Exceptions/ServiceException.cs b/SurveyBackend/SurveyBackend.Services/Exceptions/ServiceException.cs new file mode 100644 index 0000000..30fb016 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Services/Exceptions/ServiceException.cs @@ -0,0 +1,10 @@ +namespace SurveyBackend.Services.Exceptions; + +public abstract class ServiceException : Exception +{ + public abstract int StatusCode { get; } + + protected ServiceException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Services/Exceptions/UnauthorizedException.cs b/SurveyBackend/SurveyBackend.Services/Exceptions/UnauthorizedException.cs new file mode 100644 index 0000000..58ea510 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Services/Exceptions/UnauthorizedException.cs @@ -0,0 +1,10 @@ +namespace SurveyBackend.Services.Exceptions; + +public class UnauthorizedException : ServiceException +{ + public override int StatusCode => 401; + + public UnauthorizedException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Helpers/TokenHelper.cs b/SurveyBackend/SurveyBackend.Services/Helpers/TokenHelper.cs similarity index 94% rename from SurveyBackend/SurveyBackend.Infrastructure/Helpers/TokenHelper.cs rename to SurveyBackend/SurveyBackend.Services/Helpers/TokenHelper.cs index a52ae64..7c401a4 100644 --- a/SurveyBackend/SurveyBackend.Infrastructure/Helpers/TokenHelper.cs +++ b/SurveyBackend/SurveyBackend.Services/Helpers/TokenHelper.cs @@ -3,7 +3,7 @@ using System.Security.Claims; using Microsoft.IdentityModel.Tokens; using SurveyBackend.Core.Models; -namespace SurveyBackend.Infrastructure.Helpers; +namespace SurveyBackend.Services.Helpers; public class TokenHelper { diff --git a/SurveyBackend/SurveyBackend.Services/Services/AuthorizationService.cs b/SurveyBackend/SurveyBackend.Services/Services/AuthorizationService.cs new file mode 100644 index 0000000..772eb27 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Services/Services/AuthorizationService.cs @@ -0,0 +1,45 @@ +using SurveyBackend.Core.Models; +using SurveyBackend.Core.Services; +using SurveyBackend.Services.Exceptions; +using SurveyBackend.Services.Helpers; + +namespace SurveyBackend.Services.Services; + +public class AuthorizationService : IAuthorizationService +{ + private readonly IUserService _userService; + private readonly IPasswordHasher _passwordHasher; + + public AuthorizationService(IUserService userService, IPasswordHasher passwordHasher) + { + _userService = userService; + _passwordHasher = passwordHasher; + } + + public async Task LogInUser(string email, string password) + { + var user = await _userService.GetUserByEmail(email); + if (!_passwordHasher.Verify(password, user.Password)) + { + throw new UnauthorizedException("Password is incorrect."); + } + + var token = TokenHelper.GetAuthToken(user); + return token; + } + + public async Task RegisterUser(User user) + { + var isEmailTaken = await _userService.IsEmailTaken(user.Email); + if (isEmailTaken) + { + throw new ConflictException("Email already exists"); + } + + user.Password = _passwordHasher.HashPassword(user.Password); + + await _userService.CreateUserAsync(user); + var token = TokenHelper.GetAuthToken(user); + return token; + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Services/Sha256PasswordHasher.cs b/SurveyBackend/SurveyBackend.Services/Services/Sha256PasswordHasher.cs similarity index 95% rename from SurveyBackend/SurveyBackend.Infrastructure/Services/Sha256PasswordHasher.cs rename to SurveyBackend/SurveyBackend.Services/Services/Sha256PasswordHasher.cs index f513cff..ed54508 100644 --- a/SurveyBackend/SurveyBackend.Infrastructure/Services/Sha256PasswordHasher.cs +++ b/SurveyBackend/SurveyBackend.Services/Services/Sha256PasswordHasher.cs @@ -1,6 +1,7 @@ using System.Security.Cryptography; +using SurveyBackend.Core.Services; -namespace SurveyBackend.Infrastructure.Services; +namespace SurveyBackend.Services.Services; public class Sha256PasswordHasher : IPasswordHasher { diff --git a/SurveyBackend/SurveyBackend.Services/Services/UserService.cs b/SurveyBackend/SurveyBackend.Services/Services/UserService.cs new file mode 100644 index 0000000..9c52a64 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Services/Services/UserService.cs @@ -0,0 +1,31 @@ +using SurveyBackend.Core.Models; +using SurveyBackend.Core.Repositories; +using SurveyBackend.Core.Services; +using SurveyBackend.Services.Exceptions; + +namespace SurveyBackend.Services.Services; + +public class UserService : IUserService +{ + private readonly IUserRepository _userRepository; + + public UserService(IUserRepository userRepository) + { + _userRepository = userRepository; + } + + public async Task GetUserByEmail(string email) + { + return await _userRepository.GetUserByEmail(email) ?? throw new NotFoundException("Email not found"); + } + + public async Task IsEmailTaken(string email) + { + return await _userRepository.GetUserByEmail(email) != null; + } + + public async Task CreateUserAsync(User user) + { + await _userRepository.AddAsync(user); + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Services/SurveyBackend.Services.csproj b/SurveyBackend/SurveyBackend.Services/SurveyBackend.Services.csproj new file mode 100644 index 0000000..5765782 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Services/SurveyBackend.Services.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/SurveyBackend/SurveyBackend.sln b/SurveyBackend/SurveyBackend.sln index bbfd9ab..56dba6f 100644 --- a/SurveyBackend/SurveyBackend.sln +++ b/SurveyBackend/SurveyBackend.sln @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyLib.Infrastructure.EF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyLib.Core", "..\SurveyLib\SurveyLib.Core\SurveyLib.Core.csproj", "{C17C405B-37CF-48E6-AA44-44B878F4DE56}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyBackend.Services", "SurveyBackend.Services\SurveyBackend.Services.csproj", "{3CDA6495-4FB2-4F07-8B2F-15BFD2A35181}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,5 +38,9 @@ Global {C17C405B-37CF-48E6-AA44-44B878F4DE56}.Debug|Any CPU.Build.0 = Debug|Any CPU {C17C405B-37CF-48E6-AA44-44B878F4DE56}.Release|Any CPU.ActiveCfg = Release|Any CPU {C17C405B-37CF-48E6-AA44-44B878F4DE56}.Release|Any CPU.Build.0 = Release|Any CPU + {3CDA6495-4FB2-4F07-8B2F-15BFD2A35181}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CDA6495-4FB2-4F07-8B2F-15BFD2A35181}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CDA6495-4FB2-4F07-8B2F-15BFD2A35181}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CDA6495-4FB2-4F07-8B2F-15BFD2A35181}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/SurveyLib b/SurveyLib index 7bbc78f..fe2735d 160000 --- a/SurveyLib +++ b/SurveyLib @@ -1 +1 @@ -Subproject commit 7bbc78fbd7eef3bb2497b966ea73eba31aa7032c +Subproject commit fe2735da5040501f143526a8c1af19c8023f6368