Merge branch 'features/finish-authorization-backend' into 'unstable'

Add authorization

See merge request internship-2025/survey-webapp/survey-webapp!5
This commit is contained in:
Вячеслав 2025-04-18 12:37:11 +00:00
commit 43feaae7f3
28 changed files with 1164 additions and 13 deletions

View file

@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SurveyBackend.Core.Services;
using SurveyBackend.DTOs; using SurveyBackend.DTOs;
using SurveyBackend.Mappers.UserDTOs;
namespace SurveyBackend.Controllers; namespace SurveyBackend.Controllers;
@ -7,9 +9,24 @@ namespace SurveyBackend.Controllers;
[Route("auth")] [Route("auth")]
public class AuthController : ControllerBase public class AuthController : ControllerBase
{ {
[HttpPost("login")] private readonly IAuthorizationService _authorizationService;
public async Task<IActionResult> GetToken([FromBody] UserLoginDto loginData)
public AuthController(IAuthorizationService authorizationService)
{ {
return Ok(); _authorizationService = authorizationService;
}
[HttpPost("login")]
public async Task<IActionResult> LogIn([FromBody] UserLoginDto loginData)
{
var token = await _authorizationService.LogInUser(loginData.Email, loginData.Password);
return Ok(new { token = token });
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] UserRegistrationDto registerData)
{
var token = await _authorizationService.RegisterUser(UserRegistrationMapper.UserRegistrationToModel(registerData));
return Ok(new { token = token });
} }
} }

View file

@ -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,
};
}

View file

@ -0,0 +1,49 @@
using SurveyBackend.Services.Exceptions;
namespace SurveyBackend.Middlewares;
public class ExceptionsMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionsMiddleware> _logger;
public ExceptionsMiddleware(RequestDelegate next, ILogger<ExceptionsMiddleware> 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);
}
}
}

View file

@ -2,8 +2,14 @@ using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using SurveyBackend.Core.Repositories;
using SurveyBackend.Core.Services;
using SurveyBackend.Infrastructure; using SurveyBackend.Infrastructure;
using SurveyBackend.Infrastructure.Data; 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.Repositories;
using SurveyLib.Core.Services; using SurveyLib.Core.Services;
using SurveyLib.Infrastructure.EFCore.Data; using SurveyLib.Infrastructure.EFCore.Data;
@ -27,6 +33,13 @@ public class Program
builder.Services.AddScoped<SurveyDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>()); builder.Services.AddScoped<SurveyDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IPasswordHasher, Sha256PasswordHasher>();
builder.Services.AddScoped<IAuthorizationService, AuthorizationService>();
builder.Services.AddScoped<ISurveyRepository, SurveyRepository>(); builder.Services.AddScoped<ISurveyRepository, SurveyRepository>();
builder.Services.AddScoped<ISurveyService, SurveyService>(); builder.Services.AddScoped<ISurveyService, SurveyService>();
@ -59,6 +72,8 @@ public class Program
app.UseSwaggerUI(); app.UseSwaggerUI();
} }
app.UseMiddleware<ExceptionsMiddleware>();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();

View file

@ -10,13 +10,18 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2"/> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.15" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\SurveyBackend.Core\SurveyBackend.Core.csproj" /> <ProjectReference Include="..\SurveyBackend.Core\SurveyBackend.Core.csproj" />
<ProjectReference Include="..\SurveyBackend.Infrastructure\SurveyBackend.Infrastructure.csproj" /> <ProjectReference Include="..\SurveyBackend.Infrastructure\SurveyBackend.Infrastructure.csproj" />
<ProjectReference Include="..\SurveyBackend.Services\SurveyBackend.Services.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -4,7 +4,7 @@ namespace SurveyBackend.Core.Models;
public class User public class User
{ {
public string Id { get; set; } public int Id { get; set; }
public string Email { get; set; } public string Email { get; set; }
public string FirstName { get; set; } public string FirstName { get; set; }
public string LastName { get; set; } public string LastName { get; set; }

View file

@ -4,4 +4,5 @@ namespace SurveyBackend.Core.Repositories;
public interface IUserRepository : IGenericRepository<User> public interface IUserRepository : IGenericRepository<User>
{ {
public Task<User?> GetUserByEmail(string email);
} }

View file

@ -0,0 +1,9 @@
using SurveyBackend.Core.Models;
namespace SurveyBackend.Core.Services;
public interface IAuthorizationService
{
public Task<string> LogInUser(string email, string password);
public Task<string> RegisterUser(User user);
}

View file

@ -1,4 +1,4 @@
namespace SurveyBackend.Infrastructure.Services; namespace SurveyBackend.Core.Services;
public interface IPasswordHasher public interface IPasswordHasher
{ {

View file

@ -1,6 +1,10 @@
using SurveyBackend.Core.Models;
namespace SurveyBackend.Core.Services; namespace SurveyBackend.Core.Services;
public interface IUserService public interface IUserService
{ {
public Task<User> GetUserByEmail(string email);
public Task<bool> IsEmailTaken(string email);
public Task CreateUserAsync(User user);
} }

View file

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.14" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.15" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -0,0 +1,321 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.15");
modelBuilder.Entity("GroupUser", b =>
{
b.Property<int>("GroupsId")
.HasColumnType("INTEGER");
b.Property<int>("UsersId")
.HasColumnType("INTEGER");
b.HasKey("GroupsId", "UsersId");
b.HasIndex("UsersId");
b.ToTable("GroupUser");
});
modelBuilder.Entity("SurveyBackend.Core.Models.Group", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Label")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Groups");
});
modelBuilder.Entity("SurveyBackend.Core.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FirstName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("SurveyLib.Core.Models.Answer", b =>
{
b.Property<int>("CompletionId")
.HasColumnType("INTEGER");
b.Property<int>("QuestionId")
.HasColumnType("INTEGER");
b.Property<string>("AnswerText")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("CompletionId", "QuestionId");
b.HasIndex("QuestionId");
b.ToTable("Answers");
});
modelBuilder.Entity("SurveyLib.Core.Models.AnswerVariant", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("MultipleAnswerQuestionId")
.HasColumnType("INTEGER");
b.Property<int>("QuestionId")
.HasColumnType("INTEGER");
b.Property<int?>("SingleAnswerQuestionId")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("FinishedAt")
.HasColumnType("TEXT");
b.Property<int>("SurveyId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SurveyId");
b.ToTable("Completions");
});
modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Discriminator")
.IsRequired()
.HasMaxLength(34)
.HasColumnType("TEXT");
b.Property<int>("SurveyId")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("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
}
}
}

View file

@ -0,0 +1,243 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SurveyBackend.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Groups",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Label = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Groups", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Surveys",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Title = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Surveys", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Email = table.Column<string>(type: "TEXT", nullable: false),
FirstName = table.Column<string>(type: "TEXT", nullable: false),
LastName = table.Column<string>(type: "TEXT", nullable: false),
Password = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Completions",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SurveyId = table.Column<int>(type: "INTEGER", nullable: false),
FinishedAt = table.Column<DateTime>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SurveyId = table.Column<int>(type: "INTEGER", nullable: false),
Title = table.Column<string>(type: "TEXT", nullable: false),
Discriminator = table.Column<string>(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<int>(type: "INTEGER", nullable: false),
UsersId = table.Column<int>(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<int>(type: "INTEGER", nullable: false),
QuestionId = table.Column<int>(type: "INTEGER", nullable: false),
AnswerText = table.Column<string>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
QuestionId = table.Column<int>(type: "INTEGER", nullable: false),
Text = table.Column<string>(type: "TEXT", nullable: false),
MultipleAnswerQuestionId = table.Column<int>(type: "INTEGER", nullable: true),
SingleAnswerQuestionId = table.Column<int>(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");
}
/// <inheritdoc />
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");
}
}
}

View file

@ -0,0 +1,318 @@
// <auto-generated />
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<int>("GroupsId")
.HasColumnType("INTEGER");
b.Property<int>("UsersId")
.HasColumnType("INTEGER");
b.HasKey("GroupsId", "UsersId");
b.HasIndex("UsersId");
b.ToTable("GroupUser");
});
modelBuilder.Entity("SurveyBackend.Core.Models.Group", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Label")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Groups");
});
modelBuilder.Entity("SurveyBackend.Core.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FirstName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("SurveyLib.Core.Models.Answer", b =>
{
b.Property<int>("CompletionId")
.HasColumnType("INTEGER");
b.Property<int>("QuestionId")
.HasColumnType("INTEGER");
b.Property<string>("AnswerText")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("CompletionId", "QuestionId");
b.HasIndex("QuestionId");
b.ToTable("Answers");
});
modelBuilder.Entity("SurveyLib.Core.Models.AnswerVariant", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("MultipleAnswerQuestionId")
.HasColumnType("INTEGER");
b.Property<int>("QuestionId")
.HasColumnType("INTEGER");
b.Property<int?>("SingleAnswerQuestionId")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("FinishedAt")
.HasColumnType("TEXT");
b.Property<int>("SurveyId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SurveyId");
b.ToTable("Completions");
});
modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Discriminator")
.IsRequired()
.HasMaxLength(34)
.HasColumnType("TEXT");
b.Property<int>("SurveyId")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("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
}
}
}

View file

@ -41,4 +41,9 @@ public class UserRepository : IUserRepository
_context.Users.Remove(entity); _context.Users.Remove(entity);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }
public async Task<User?> GetUserByEmail(string email)
{
return await _context.Users.FirstOrDefaultAsync(u => u.Email == email);
}
} }

View file

@ -7,7 +7,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
</ItemGroup> </ItemGroup>
@ -16,4 +20,8 @@
<ProjectReference Include="..\SurveyBackend.Core\SurveyBackend.Core.csproj" /> <ProjectReference Include="..\SurveyBackend.Core\SurveyBackend.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Data\Migrations\" />
</ItemGroup>
</Project> </Project>

View file

@ -2,7 +2,7 @@ using System.Text;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
namespace SurveyBackend.Infrastructure; namespace SurveyBackend.Services;
public static class AuthOptions public static class AuthOptions
{ {

View file

@ -0,0 +1,10 @@
namespace SurveyBackend.Services.Exceptions;
public class ConflictException : ServiceException
{
public override int StatusCode => 409;
public ConflictException(string message) : base(message)
{
}
}

View file

@ -0,0 +1,10 @@
namespace SurveyBackend.Services.Exceptions;
public class NotFoundException : ServiceException
{
public override int StatusCode => 404;
public NotFoundException(string message) : base(message)
{
}
}

View file

@ -0,0 +1,10 @@
namespace SurveyBackend.Services.Exceptions;
public abstract class ServiceException : Exception
{
public abstract int StatusCode { get; }
protected ServiceException(string message) : base(message)
{
}
}

View file

@ -0,0 +1,10 @@
namespace SurveyBackend.Services.Exceptions;
public class UnauthorizedException : ServiceException
{
public override int StatusCode => 401;
public UnauthorizedException(string message) : base(message)
{
}
}

View file

@ -3,7 +3,7 @@ using System.Security.Claims;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using SurveyBackend.Core.Models; using SurveyBackend.Core.Models;
namespace SurveyBackend.Infrastructure.Helpers; namespace SurveyBackend.Services.Helpers;
public class TokenHelper public class TokenHelper
{ {

View file

@ -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<string> 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<string> 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;
}
}

View file

@ -1,6 +1,7 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using SurveyBackend.Core.Services;
namespace SurveyBackend.Infrastructure.Services; namespace SurveyBackend.Services.Services;
public class Sha256PasswordHasher : IPasswordHasher public class Sha256PasswordHasher : IPasswordHasher
{ {

View file

@ -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<User> GetUserByEmail(string email)
{
return await _userRepository.GetUserByEmail(email) ?? throw new NotFoundException("Email not found");
}
public async Task<bool> IsEmailTaken(string email)
{
return await _userRepository.GetUserByEmail(email) != null;
}
public async Task CreateUserAsync(User user)
{
await _userRepository.AddAsync(user);
}
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SurveyBackend.Core\SurveyBackend.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
</ItemGroup>
</Project>

View file

@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyLib.Infrastructure.EF
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyLib.Core", "..\SurveyLib\SurveyLib.Core\SurveyLib.Core.csproj", "{C17C405B-37CF-48E6-AA44-44B878F4DE56}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyLib.Core", "..\SurveyLib\SurveyLib.Core\SurveyLib.Core.csproj", "{C17C405B-37CF-48E6-AA44-44B878F4DE56}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyBackend.Services", "SurveyBackend.Services\SurveyBackend.Services.csproj", "{3CDA6495-4FB2-4F07-8B2F-15BFD2A35181}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{C17C405B-37CF-48E6-AA44-44B878F4DE56}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

@ -1 +1 @@
Subproject commit 7bbc78fbd7eef3bb2497b966ea73eba31aa7032c Subproject commit fe2735da5040501f143526a8c1af19c8023f6368