Merge branch 'unstable' into 'main'

v0.0.1

See merge request internship-2025/survey-webapp/survey-webapp!8
This commit is contained in:
Вячеслав 2025-04-23 17:30:45 +00:00
commit c6f6019af2
124 changed files with 4203 additions and 229 deletions

25
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,25 @@
stages:
- .pre
- build
- test
- deploy
variables:
GIT_SUBMODULE_STRATEGY: recursive
PUBLISH_DIR: BackendPublish
top-level-job:
stage: .pre
image: alpine:latest
script:
- echo "Starting pipeline"
include:
- local: '/SurveyBackend/backend.gitlab-ci.yml'
rules:
- changes:
- 'SurveyBackend/*'
- local: '/SurveyFrontend/frontend.gitlab-ci.yml'
rules:
- changes:
- 'SurveyFrontend/*'

View file

@ -0,0 +1,19 @@
using System.Security.Claims;
using SurveyBackend.Core.Contexts;
namespace SurveyBackend.Contexts;
public class UserContext : IUserContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
public UserContext(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public int UserId =>
int.Parse(
_httpContextAccessor.HttpContext?.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)
?.Value ?? throw new UnauthorizedAccessException());
}

View file

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SurveyBackend.DTOs;
using SurveyBackend.Mappers;
using IAuthorizationService = SurveyBackend.Core.Services.IAuthorizationService;
namespace SurveyBackend.Controllers;
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
private readonly IAuthorizationService _authorizationService;
public AuthController(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
[AllowAnonymous]
[HttpPost("login")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> LogIn([FromBody] UserLoginDto loginData)
{
var token = await _authorizationService.LogInUser(loginData.Email, loginData.Password);
return Ok(new { token = token });
}
[AllowAnonymous]
[HttpPost("register")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Register([FromBody] UserRegistrationDto registerData)
{
var token = await _authorizationService.RegisterUser(
AuthMapper.UserRegistrationToModel(registerData));
return Ok(new { token = token });
}
}

View file

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SurveyBackend.Core.Contexts;
using SurveyBackend.DTOs.Question;
using SurveyBackend.Mappers;
using SurveyLib.Core.Services;
namespace SurveyBackend.Controllers;
[ApiController]
[Route("api/surveys/{surveyId}/questions")]
public class QuestionController : ControllerBase
{
private readonly IQuestionService _questionService;
public QuestionController(IQuestionService questionService, IUserContext userContext)
{
_questionService = questionService;
}
[AllowAnonymous]
[HttpGet]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(List<OutputQuestionDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetBySurveyId([FromRoute] int surveyId)
{
var questions = await _questionService.GetQuestionsBySurveyIdAsync(surveyId);
var result = questions.Select(QuestionMapper.ModelToQuestionDto).ToList();
return Ok(result);
}
[Authorize]
[HttpPost]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> AddQuestion(CreateQuestionDto dto, [FromRoute] int surveyId)
{
var model = QuestionMapper.QuestionCreationToModel(dto, surveyId);
await _questionService.AddQuestionAsync(model);
return Created();
}
}

View file

@ -0,0 +1,81 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SurveyBackend.Core.Contexts;
using SurveyBackend.DTOs.Survey;
using SurveyBackend.Mappers;
using SurveyBackend.Services.Exceptions;
using SurveyLib.Core.Models;
using SurveyLib.Core.Services;
namespace SurveyBackend.Controllers;
[ApiController]
[Route("api/surveys")]
public class SurveyController : ControllerBase
{
private readonly ISurveyService _surveyService;
private readonly IUserContext _userContext;
public SurveyController(ISurveyService surveyService, IUserContext userContext)
{
_surveyService = surveyService;
_userContext = userContext;
}
[AllowAnonymous]
[HttpGet]
[ProducesResponseType(typeof(List<OutputSurveyDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> Get()
{
var surveys = await _surveyService.GetSurveysAsync();
var result = surveys.Select(SurveyMapper.ModelToOutputDto);
return Ok(result);
}
[AllowAnonymous]
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(OutputSurveyDto), StatusCodes.Status200OK)]
public async Task<IActionResult> Get(int id)
{
var survey = await _surveyService.GetSurveyAsync(id);
var result = SurveyMapper.ModelToOutputDto(survey);
return Ok(result);
}
[Authorize]
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Post([FromBody] CreateSurveyDto dto)
{
var userId = _userContext.UserId;
var survey = SurveyMapper.CreateDtoToModel(dto, userId);
await _surveyService.AddSurveyAsync(survey);
return Created();
}
[Authorize]
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Delete(int id)
{
await _surveyService.DeleteSurveyAsync(id);
return Ok();
}
[Authorize]
[HttpGet("my")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(List<OutputSurveyDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetMySurveys()
{
var userId = _userContext.UserId;
var result = await _surveyService.GetSurveysByUserIdAsync(userId);
return Ok(result);
}
}

View file

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using SurveyLib.Core.Services;
namespace SurveyBackend.Controllers;
[ApiController]
[Route("api/test")]
public class TestController : ControllerBase
{
}

View file

@ -0,0 +1,9 @@
namespace SurveyBackend.DTOs.Question;
public class CreateQuestionDto
{
public required string Title { get; set; }
public required string QuestionType { get; set; }
public List<string>? AnswerVariants { get; set; }
}

View file

@ -0,0 +1,8 @@
namespace SurveyBackend.DTOs.Question;
public class OutputAnswerVariantDto
{
public required int Id { get; set; }
public required int QuestionId { get; set; }
public required string Text { get; set; }
}

View file

@ -0,0 +1,10 @@
namespace SurveyBackend.DTOs.Question;
public class OutputQuestionDto
{
public required int Id { get; set; }
public required int SurveyId { get; set; }
public required string Title { get; set; }
public required string QuestionType { get; set; }
public List<OutputAnswerVariantDto>? AnswerVariants { get; set; }
}

View file

@ -0,0 +1,7 @@
namespace SurveyBackend.DTOs.Survey;
public class CreateSurveyDto
{
public required string Title { get; set; }
public string Description { get; set; } = string.Empty;
}

View file

@ -0,0 +1,9 @@
namespace SurveyBackend.DTOs.Survey;
public class OutputSurveyDto
{
public required int Id { get; set; }
public required string Title { get; set; }
public required string Description { get; set; }
public int? CreatedBy { get; set; }
}

View file

@ -0,0 +1,7 @@
namespace SurveyBackend.DTOs;
public record UserLoginDto
{
public required string Email { get; set; }
public required string Password { get; set; }
}

View file

@ -0,0 +1,10 @@
namespace SurveyBackend.DTOs;
public record UserRegistrationDto
{
public string Email { get; set; }
public string Username { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Password { get; set; }
}

View file

@ -0,0 +1,15 @@
using SurveyBackend.Core.Models;
using SurveyBackend.DTOs;
namespace SurveyBackend.Mappers;
public static class AuthMapper
{
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,57 @@
using SurveyBackend.DTOs.Question;
using SurveyBackend.Services.Exceptions;
using SurveyLib.Core.Models;
using SurveyLib.Core.Models.QuestionVariants;
namespace SurveyBackend.Mappers;
public static class QuestionMapper
{
public static QuestionBase QuestionCreationToModel(CreateQuestionDto dto, int surveyId)
{
return dto.QuestionType.ToLower() switch
{
"textquestion" => new TextQuestion
{
Title = dto.Title,
SurveyId = surveyId,
},
"singleanswerquestion" => new SingleAnswerQuestion
{
Title = dto.Title,
SurveyId = surveyId,
AnswerVariants = dto.AnswerVariants?.Select(v => new AnswerVariant { Text = v }).ToList() ?? [],
},
"multipleanswerquestion" => new MultipleAnswerQuestion
{
Title = dto.Title,
SurveyId = surveyId,
AnswerVariants = dto.AnswerVariants?.Select(v => new AnswerVariant { Text = v }).ToList() ?? []
},
_ => throw new BadRequestException("Unknown question type")
};
}
public static OutputQuestionDto ModelToQuestionDto(QuestionBase question)
{
var withAnswerVariants = question.GetType() != typeof(TextQuestion);
return new OutputQuestionDto
{
Id = question.Id,
SurveyId = question.SurveyId,
Title = question.Title,
QuestionType = question.Discriminator,
AnswerVariants = withAnswerVariants ? AnswerVariantsToDto(question.AnswerVariants) : null,
};
}
private static List<OutputAnswerVariantDto> AnswerVariantsToDto(IEnumerable<AnswerVariant> answerVariants)
{
return answerVariants.Select(av => new OutputAnswerVariantDto
{
Id = av.Id,
QuestionId = av.QuestionId,
Text = av.Text,
}).ToList();
}
}

View file

@ -0,0 +1,28 @@
using SurveyBackend.DTOs.Survey;
using SurveyLib.Core.Models;
namespace SurveyBackend.Mappers;
public static class SurveyMapper
{
public static Survey CreateDtoToModel(CreateSurveyDto dto, int userId)
{
return new Survey
{
Title = dto.Title,
Description = dto.Description,
CreatedBy = userId
};
}
public static OutputSurveyDto ModelToOutputDto(Survey survey)
{
return new OutputSurveyDto
{
Id = survey.Id,
Title = survey.Title,
Description = survey.Description,
CreatedBy = survey.CreatedBy,
};
}
}

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

@ -0,0 +1,124 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using SurveyBackend.Contexts;
using SurveyBackend.Core.Contexts;
using SurveyBackend.Core.Repositories;
using SurveyBackend.Core.Services;
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;
using SurveyLib.Infrastructure.EFCore.Repositories;
namespace SurveyBackend;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
AuthOptions.MakeOptions(builder.Configuration, Environment.GetEnvironmentVariable("JWT_SECRET_KEY"));
builder.Services.AddAuthorization();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<SurveyDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<IUserContext, UserContext>();
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<IQuestionRepository, QuestionRepository>();
builder.Services.AddScoped<ISurveyService, SurveyService>();
builder.Services.AddScoped<IQuestionService, QuestionService>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = AuthOptions.Issuer,
ValidAudience = AuthOptions.Audience,
IssuerSigningKey = AuthOptions.SymmetricSecurityKey
};
});
builder.Services.AddControllers().AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme.",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = JwtBearerDefaults.AuthenticationScheme
}
},
Array.Empty<string>()
}
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger(options => { options.RouteTemplate = "api/swagger/{documentName}/swagger.json"; });
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/api/swagger/v1/swagger.json", "Survey Backend V1");
options.RoutePrefix = "api/swagger";
});
}
app.UseMiddleware<ExceptionsMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}

View file

@ -13,7 +13,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "swagger", "launchUrl": "api/swagger",
"applicationUrl": "http://localhost:5231", "applicationUrl": "http://localhost:5231",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"

View file

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>SurveyBackend</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Content Update="appsettings.*.json">
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2"/>
<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"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SurveyBackend.Core\SurveyBackend.Core.csproj"/>
<ProjectReference Include="..\SurveyBackend.Infrastructure\SurveyBackend.Infrastructure.csproj"/>
<ProjectReference Include="..\SurveyBackend.Services\SurveyBackend.Services.csproj"/>
</ItemGroup>
</Project>

View file

@ -0,0 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Data Source=Application.db"
},
"JwtSettings": {
"SecretKey": "sigma_super_secret_key_for_jwt_tokens_yo",
"Issuer": "SurveyBackend",
"Audience": "SurveyClient",
"ExpiresInMinutes": 600
}
}

View file

@ -0,0 +1,18 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Data Source=Application.db"
},
"JwtSettings": {
"SecretKey": "sigma_super_secret_key_for_jwt_tokens_yo_that_should_be_stored_in_ENV",
"Issuer": "SurveyBackend",
"Audience": "SurveyClient",
"ExpiresInMinutes": 600
}
}

View file

@ -0,0 +1,6 @@
namespace SurveyBackend.Core.Contexts;
public interface IUserContext
{
int UserId { get; }
}

View file

@ -0,0 +1,9 @@
namespace SurveyBackend.Core.Models;
public class Group
{
public int Id { get; set; }
public string Label { get; set; }
public ICollection<User> Users { get; set; }
}

View file

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Identity;
namespace SurveyBackend.Core.Models;
public class User
{
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Password { get; set; }
public ICollection<Group> Groups { get; set; }
}

View file

@ -0,0 +1,10 @@
namespace SurveyBackend.Core.Repositories;
public interface IGenericRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}

View file

@ -0,0 +1,8 @@
using SurveyBackend.Core.Models;
namespace SurveyBackend.Core.Repositories;
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

@ -0,0 +1,7 @@
namespace SurveyBackend.Core.Services;
public interface IPasswordHasher
{
public string HashPassword(string password);
public bool Verify(string password, string hashedPassword);
}

View file

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

View file

@ -1,14 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2"/> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.15" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using SurveyBackend.Core.Models;
using SurveyLib.Core.Models;
using SurveyLib.Infrastructure.EFCore.Data;
namespace SurveyBackend.Infrastructure.Data;
public class ApplicationDbContext : SurveyDbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Group> Groups { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Survey>()
.HasOne<User>()
.WithMany()
.HasForeignKey(s => s.CreatedBy)
.OnDelete(DeleteBehavior.SetNull);
}
}

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,334 @@
// <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("20250418190738_Add CreatedBy to Survey")]
partial class AddCreatedBytoSurvey
{
/// <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<int?>("CreatedBy")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedBy");
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.Survey", b =>
{
b.HasOne("SurveyBackend.Core.Models.User", null)
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.SetNull);
});
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,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SurveyBackend.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddCreatedBytoSurvey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "CreatedBy",
table: "Surveys",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Surveys_CreatedBy",
table: "Surveys",
column: "CreatedBy");
migrationBuilder.AddForeignKey(
name: "FK_Surveys_Users_CreatedBy",
table: "Surveys",
column: "CreatedBy",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Surveys_Users_CreatedBy",
table: "Surveys");
migrationBuilder.DropIndex(
name: "IX_Surveys_CreatedBy",
table: "Surveys");
migrationBuilder.DropColumn(
name: "CreatedBy",
table: "Surveys");
}
}
}

View file

@ -0,0 +1,78 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SurveyBackend.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class Changingquestionslogic : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AnswerVariant_Questions_MultipleAnswerQuestionId",
table: "AnswerVariant");
migrationBuilder.DropForeignKey(
name: "FK_AnswerVariant_Questions_SingleAnswerQuestionId",
table: "AnswerVariant");
migrationBuilder.DropIndex(
name: "IX_AnswerVariant_MultipleAnswerQuestionId",
table: "AnswerVariant");
migrationBuilder.DropIndex(
name: "IX_AnswerVariant_SingleAnswerQuestionId",
table: "AnswerVariant");
migrationBuilder.DropColumn(
name: "MultipleAnswerQuestionId",
table: "AnswerVariant");
migrationBuilder.DropColumn(
name: "SingleAnswerQuestionId",
table: "AnswerVariant");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "MultipleAnswerQuestionId",
table: "AnswerVariant",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "SingleAnswerQuestionId",
table: "AnswerVariant",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_AnswerVariant_MultipleAnswerQuestionId",
table: "AnswerVariant",
column: "MultipleAnswerQuestionId");
migrationBuilder.CreateIndex(
name: "IX_AnswerVariant_SingleAnswerQuestionId",
table: "AnswerVariant",
column: "SingleAnswerQuestionId");
migrationBuilder.AddForeignKey(
name: "FK_AnswerVariant_Questions_MultipleAnswerQuestionId",
table: "AnswerVariant",
column: "MultipleAnswerQuestionId",
principalTable: "Questions",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_AnswerVariant_Questions_SingleAnswerQuestionId",
table: "AnswerVariant",
column: "SingleAnswerQuestionId",
principalTable: "Questions",
principalColumn: "Id");
}
}
}

View file

@ -0,0 +1,305 @@
// <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>("QuestionId")
.HasColumnType("INTEGER");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("QuestionId");
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<int?>("CreatedBy")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedBy");
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.QuestionBase", "Question")
.WithMany("AnswerVariants")
.HasForeignKey("QuestionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
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.Survey", b =>
{
b.HasOne("SurveyBackend.Core.Models.User", null)
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("SurveyLib.Core.Models.Completion", b =>
{
b.Navigation("Answers");
});
modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b =>
{
b.Navigation("AnswerVariants");
b.Navigation("Answers");
});
modelBuilder.Entity("SurveyLib.Core.Models.Survey", b =>
{
b.Navigation("Completions");
b.Navigation("Questions");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore;
using SurveyBackend.Core.Models;
using SurveyBackend.Core.Repositories;
using SurveyBackend.Infrastructure.Data;
namespace SurveyBackend.Infrastructure.Repositories;
public class UserRepository : IUserRepository
{
private readonly ApplicationDbContext _context;
public UserRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<User?> GetByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
public async Task<IEnumerable<User>> GetAllAsync()
{
return await _context.Users.ToListAsync();
}
public async Task AddAsync(User entity)
{
await _context.Users.AddAsync(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(User entity)
{
_context.Users.Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(User entity)
{
_context.Users.Remove(entity);
await _context.SaveChangesAsync();
}
public async Task<User?> GetUserByEmail(string email)
{
return await _context.Users.FirstOrDefaultAsync(u => u.Email == email);
}
}

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SurveyLib\SurveyLib.Infrastructure.EFCore\SurveyLib.Infrastructure.EFCore.csproj" />
<ProjectReference Include="..\SurveyBackend.Core\SurveyBackend.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\Migrations\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,32 @@
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
namespace SurveyBackend.Services;
public static class AuthOptions
{
public static string Issuer;
public static string Audience;
public static TimeSpan TokenLifetime;
private static string? SecurityKey { get; set; }
public static SymmetricSecurityKey SymmetricSecurityKey
{
get
{
ArgumentNullException.ThrowIfNull(SecurityKey);
return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey));
}
}
public static void MakeOptions(IConfigurationManager configurationManager, string? securityKey = null)
{
var jwtSettings = configurationManager.GetSection("JwtSettings");
Issuer = jwtSettings["Issuer"] ?? "DefaultIssuer";
Audience = jwtSettings["Audience"] ?? "DefaultAudience";
TokenLifetime = TimeSpan.FromMinutes(int.Parse(jwtSettings["TokenLifetime"] ?? "60"));
SecurityKey = securityKey ?? jwtSettings["SecretKey"];
}
}

View file

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

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

@ -0,0 +1,31 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using SurveyBackend.Core.Models;
namespace SurveyBackend.Services.Helpers;
public class TokenHelper
{
public static string GetAuthToken(User user)
{
var userId = user.Id.ToString();
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, userId)
};
var jwt = new JwtSecurityToken(
claims: claims,
issuer: AuthOptions.Issuer,
audience: AuthOptions.Audience,
expires: DateTime.UtcNow + AuthOptions.TokenLifetime,
signingCredentials: new SigningCredentials(AuthOptions.SymmetricSecurityKey, SecurityAlgorithms.HmacSha256)
);
var token = new JwtSecurityTokenHandler().WriteToken(jwt);
return token;
}
}

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

@ -0,0 +1,63 @@
using SurveyBackend.Core.Contexts;
using SurveyBackend.Services.Exceptions;
using SurveyLib.Core.Models;
using SurveyLib.Core.Repositories;
using SurveyLib.Core.Services;
namespace SurveyBackend.Services.Services;
public class QuestionService : IQuestionService
{
private readonly IQuestionRepository _questionRepository;
private readonly ISurveyRepository _surveyRepository;
public QuestionService(IQuestionRepository questionRepository, ISurveyRepository surveyRepository,
IUserContext userContext)
{
_questionRepository = questionRepository;
_surveyRepository = surveyRepository;
}
public async Task AddQuestionAsync(QuestionBase question)
{
await _questionRepository.AddAsync(question);
}
public async Task UpdateQuestionAsync(QuestionBase question)
{
await _questionRepository.UpdateAsync(question);
}
public async Task DeleteQuestionAsync(int id)
{
var question = await _questionRepository.GetByIdAsync(id);
if (question is null)
{
throw new NotFoundException("Question not found");
}
await _questionRepository.DeleteAsync(question);
}
public async Task<QuestionBase> GetQuestionByIdAsync(int id)
{
var question = await _questionRepository.GetByIdAsync(id);
if (question is null)
{
throw new NotFoundException("Question not found");
}
return question;
}
public async Task<IEnumerable<QuestionBase>> GetQuestionsBySurveyIdAsync(int surveyId)
{
var survey = await _surveyRepository.GetByIdAsync(surveyId);
if (survey is null)
{
throw new NotFoundException("Survey not found");
}
return await _questionRepository.GetQuestionsBySurveyId(surveyId);
}
}

View file

@ -0,0 +1,62 @@
using System.Security.Cryptography;
using SurveyBackend.Core.Services;
namespace SurveyBackend.Services.Services;
public class Sha256PasswordHasher : IPasswordHasher
{
private const int RehashCount = 10000;
private const int HashSizeBytes = 16;
private const int SaltSizeBytes = 16;
public string HashPassword(string password)
{
var saltBytes = RandomNumberGenerator.GetBytes(SaltSizeBytes);
using var deriveBytes = GetDeriveBytes(password, saltBytes);
var hashBytes = deriveBytes.GetBytes(HashSizeBytes);
var fullHashBytes = new byte[SaltSizeBytes + HashSizeBytes];
Array.Copy(saltBytes, 0, fullHashBytes, 0, SaltSizeBytes);
Array.Copy(hashBytes, 0, fullHashBytes, SaltSizeBytes, HashSizeBytes);
var hashedPassword = Convert.ToBase64String(fullHashBytes);
return hashedPassword;
}
public bool Verify(string password, string hashedPassword)
{
var fullHashBytes = Convert.FromBase64String(hashedPassword);
var saltBytes = new byte[SaltSizeBytes];
Array.Copy(fullHashBytes, 0, saltBytes, 0, SaltSizeBytes);
using var deriveBytes = GetDeriveBytes(password, saltBytes);
var hashBytes = deriveBytes.GetBytes(HashSizeBytes);
var isMatch = CompareHashes(hashBytes, fullHashBytes);
return isMatch;
}
private static bool CompareHashes(byte[] hashBytes, byte[] fullHashBytes)
{
for (var i = 0; i < HashSizeBytes; i++)
if (hashBytes[i] != fullHashBytes[i + SaltSizeBytes])
return false;
return true;
}
private static Rfc2898DeriveBytes GetDeriveBytes(string password, byte[] saltBytes)
{
return new Rfc2898DeriveBytes(password, saltBytes, RehashCount, HashAlgorithmName.SHA256);
}
}

View file

@ -0,0 +1,69 @@
using SurveyBackend.Core.Contexts;
using SurveyBackend.Core.Repositories;
using SurveyBackend.Services.Exceptions;
using SurveyLib.Core.Models;
using SurveyLib.Core.Repositories;
using SurveyLib.Core.Services;
namespace SurveyBackend.Services.Services;
public class SurveyService : ISurveyService
{
private readonly ISurveyRepository _surveyRepository;
private readonly IUserContext _userContext;
public SurveyService(ISurveyRepository surveyRepository, IUserContext userContext)
{
_surveyRepository = surveyRepository;
_userContext = userContext;
}
public async Task AddSurveyAsync(Survey survey)
{
await _surveyRepository.AddAsync(survey);
}
public async Task UpdateSurveyAsync(Survey survey)
{
if (survey.CreatedBy != _userContext.UserId)
throw new UnauthorizedAccessException("You are not authorized to update this survey.");
await _surveyRepository.UpdateAsync(survey);
}
public async Task DeleteSurveyAsync(int id)
{
var survey = await _surveyRepository.GetByIdAsync(id);
if (survey is null)
{
throw new NotFoundException("Survey not found");
}
if (survey.CreatedBy != _userContext.UserId)
{
throw new UnauthorizedAccessException("You are not authorized to delete this survey.");
}
await _surveyRepository.DeleteAsync(survey);
}
public async Task<IEnumerable<Survey>> GetSurveysAsync()
{
return await _surveyRepository.GetAllAsync();
}
public async Task<Survey> GetSurveyAsync(int id)
{
var survey = await _surveyRepository.GetByIdAsync(id);
if (survey is null)
{
throw new NotFoundException("Survey not found");
}
return survey;
}
public async Task<IEnumerable<Survey>> GetSurveysByUserIdAsync(int userId)
{
return await _surveyRepository.GetSurveysByUserIdAsync(userId);
}
}

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,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SurveyLib\SurveyLib.Core\SurveyLib.Core.csproj" />
<ProjectReference Include="..\SurveyBackend.Core\SurveyBackend.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
</ItemGroup>
</Project>

View file

@ -1,6 +1,16 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyBackend", "SurveyBackend\SurveyBackend.csproj", "{2941E98A-5311-4B97-B8B0-8DBF5E1C3B56}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyBackend.API", "SurveyBackend.API\SurveyBackend.API.csproj", "{2941E98A-5311-4B97-B8B0-8DBF5E1C3B56}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyBackend.Core", "SurveyBackend.Core\SurveyBackend.Core.csproj", "{596B4603-4066-4FF2-9C96-5357193F7229}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyBackend.Infrastructure", "SurveyBackend.Infrastructure\SurveyBackend.Infrastructure.csproj", "{4006471D-9F65-4AD6-852B-88A1211B49F4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyLib.Infrastructure.EFCore", "..\SurveyLib\SurveyLib.Infrastructure.EFCore\SurveyLib.Infrastructure.EFCore.csproj", "{CD9FE310-CDD1-4661-AB41-E606D35E1694}"
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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -12,5 +22,25 @@ Global
{2941E98A-5311-4B97-B8B0-8DBF5E1C3B56}.Debug|Any CPU.Build.0 = Debug|Any CPU {2941E98A-5311-4B97-B8B0-8DBF5E1C3B56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2941E98A-5311-4B97-B8B0-8DBF5E1C3B56}.Release|Any CPU.ActiveCfg = Release|Any CPU {2941E98A-5311-4B97-B8B0-8DBF5E1C3B56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2941E98A-5311-4B97-B8B0-8DBF5E1C3B56}.Release|Any CPU.Build.0 = Release|Any CPU {2941E98A-5311-4B97-B8B0-8DBF5E1C3B56}.Release|Any CPU.Build.0 = Release|Any CPU
{596B4603-4066-4FF2-9C96-5357193F7229}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{596B4603-4066-4FF2-9C96-5357193F7229}.Debug|Any CPU.Build.0 = Debug|Any CPU
{596B4603-4066-4FF2-9C96-5357193F7229}.Release|Any CPU.ActiveCfg = Release|Any CPU
{596B4603-4066-4FF2-9C96-5357193F7229}.Release|Any CPU.Build.0 = Release|Any CPU
{4006471D-9F65-4AD6-852B-88A1211B49F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4006471D-9F65-4AD6-852B-88A1211B49F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4006471D-9F65-4AD6-852B-88A1211B49F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4006471D-9F65-4AD6-852B-88A1211B49F4}.Release|Any CPU.Build.0 = Release|Any CPU
{CD9FE310-CDD1-4661-AB41-E606D35E1694}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD9FE310-CDD1-4661-AB41-E606D35E1694}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD9FE310-CDD1-4661-AB41-E606D35E1694}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD9FE310-CDD1-4661-AB41-E606D35E1694}.Release|Any CPU.Build.0 = Release|Any CPU
{C17C405B-37CF-48E6-AA44-44B878F4DE56}.Debug|Any CPU.ActiveCfg = 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.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

View file

@ -1,51 +0,0 @@
namespace SurveyBackend;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthorization();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", (HttpContext httpContext) =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
})
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
}
}

View file

@ -1,12 +0,0 @@
namespace SurveyBackend;
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}

View file

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View file

@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View file

@ -0,0 +1,59 @@
stages:
- build
- test
- deploy
backend-build-job:
image: mcr.microsoft.com/dotnet/sdk:8.0
stage: build
script:
- dotnet restore SurveyBackend/SurveyBackend.sln
- dotnet tool install --global dotnet-ef
- export PATH="$PATH:/root/.dotnet/tools"
- >
dotnet ef migrations bundle
--self-contained
-r linux-x64
-p SurveyBackend/SurveyBackend.Infrastructure/SurveyBackend.Infrastructure.csproj
-s ./SurveyBackend/SurveyBackend.API/SurveyBackend.API.csproj
-o $PUBLISH_DIR/efbundle
- >
dotnet publish SurveyBackend/SurveyBackend.API/SurveyBackend.API.csproj
-c Release -r linux-x64 --self-contained true
/p:PublishReadyToRun=true
/p:PublishSingleFile=true
/p:IncludeNativeLibrariesForSelfExtract=false
/p:TrimUnusedDependencies=false
-o $PUBLISH_DIR
artifacts:
paths:
- $PUBLISH_DIR
expire_in: 1 hour
backend-test-job:
image: alpine:latest
stage: test
script:
- echo "Here we would run some tests if I knew how to use them"
backend-deploy-job:
image: alpine:latest
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
before_script:
- apk add --no-cache openssh
- mkdir -p ~/.ssh
- echo "$DEPLOY_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
script:
- echo "Deploying backend to $DEPLOY_HOST"
- scp -r $PUBLISH_DIR/. $DEPLOY_USER@$DEPLOY_HOST:$BACKEND_DEPLOY_PATH
- >
ssh $DEPLOY_USER@$DEPLOY_HOST "
cd $BACKEND_DEPLOY_PATH &&
echo 'Running migrations' &&
./efbundle &&
echo 'Restarting systemd service' &&
systemctl --user restart SurveyBackend.service"

View file

@ -0,0 +1,37 @@
stages:
- build
- test
- deploy
frontend-build-job:
image: node:20-alpine
stage: build
script:
- cd SurveyFrontend
- npm install
- npm run build
artifacts:
paths:
- SurveyFrontend/dist
expire_in: 1 hour
frontend-test-job:
image: alpine:latest
stage: test
script:
- echo "Here we would run some tests if I knew how to use them"
frontend-deploy-job:
image: alpine:latest
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
before_script:
- apk add --no-cache openssh
- mkdir -p ~/.ssh
- echo "$DEPLOY_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
script:
- echo "Deploying frontend to $DEPLOY_HOST"
- scp -r SurveyFrontend/dist/* $DEPLOY_USER@$DEPLOY_HOST:$FRONTEND_DEPLOY_PATH

View file

@ -2,9 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Survey</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -8,8 +8,10 @@
"name": "survey-frontend", "name": "survey-frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@formkit/tempo": "^0.1.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "@eslint/js": "^9.21.0",
@ -841,6 +843,12 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@formkit/tempo": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@formkit/tempo/-/tempo-0.1.2.tgz",
"integrity": "sha512-jNPPbjL8oj7hK3eHX++CwbR6X4GKQt+x00/q4yeXkwynXHGKL27dylYhpEgwrmediPP4y7s0XtN1if/M/JYujg==",
"license": "MIT"
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -2876,6 +2884,19 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "6.2.2", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",

View file

@ -10,8 +10,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@formkit/tempo": "^0.1.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "@eslint/js": "^9.21.0",

View file

@ -0,0 +1,3 @@
<svg width="52" height="52" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.675 37.05C14.5167 35.6417 16.575 34.5312 18.85 33.7188C21.125 32.9062 23.5084 32.5 26 32.5C28.4917 32.5 30.875 32.9062 33.15 33.7188C35.425 34.5312 37.4834 35.6417 39.325 37.05C40.5889 35.5694 41.573 33.8903 42.2771 32.0125C42.9813 30.1347 43.3334 28.1306 43.3334 26C43.3334 21.1972 41.6452 17.1076 38.2688 13.7312C34.8924 10.3549 30.8028 8.66667 26 8.66667C21.1973 8.66667 17.1077 10.3549 13.7313 13.7312C10.3549 17.1076 8.66671 21.1972 8.66671 26C8.66671 28.1306 9.01879 30.1347 9.72296 32.0125C10.4271 33.8903 11.4112 35.5694 12.675 37.05ZM26 28.1667C23.8695 28.1667 22.073 27.4354 20.6105 25.9729C19.148 24.5104 18.4167 22.7139 18.4167 20.5833C18.4167 18.4528 19.148 16.6562 20.6105 15.1937C22.073 13.7312 23.8695 13 26 13C28.1306 13 29.9271 13.7312 31.3896 15.1937C32.8521 16.6562 33.5834 18.4528 33.5834 20.5833C33.5834 22.7139 32.8521 24.5104 31.3896 25.9729C29.9271 27.4354 28.1306 28.1667 26 28.1667ZM26 47.6667C23.0028 47.6667 20.1862 47.0979 17.55 45.9604C14.9139 44.8229 12.6209 43.2792 10.6709 41.3292C8.72087 39.3792 7.17712 37.0861 6.03962 34.45C4.90212 31.8139 4.33337 28.9972 4.33337 26C4.33337 23.0028 4.90212 20.1861 6.03962 17.55C7.17712 14.9139 8.72087 12.6208 10.6709 10.6708C12.6209 8.72083 14.9139 7.17708 17.55 6.03958C20.1862 4.90208 23.0028 4.33333 26 4.33333C28.9973 4.33333 31.8139 4.90208 34.45 6.03958C37.0862 7.17708 39.3792 8.72083 41.3292 10.6708C43.2792 12.6208 44.823 14.9139 45.9605 17.55C47.098 20.1861 47.6667 23.0028 47.6667 26C47.6667 28.9972 47.098 31.8139 45.9605 34.45C44.823 37.0861 43.2792 39.3792 41.3292 41.3292C39.3792 43.2792 37.0862 44.8229 34.45 45.9604C31.8139 47.0979 28.9973 47.6667 26 47.6667Z" fill="#79747E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,4 @@
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.1579 1.5V14.5" stroke="#3788D6" stroke-width="2" stroke-linecap="round"/>
<path d="M14 8L1 8" stroke="#3788D6" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 268 B

View file

@ -0,0 +1,3 @@
<svg width="28" height="29" viewBox="0 0 28 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.8334 20.3333H15.1667V15.6667H19.8334V13.3333H15.1667V8.66668H12.8334V13.3333H8.16671V15.6667H12.8334V20.3333ZM14 26.1667C12.3862 26.1667 10.8695 25.8653 9.45004 25.2625C8.0306 24.6403 6.79587 23.8042 5.74587 22.7542C4.69587 21.7042 3.85976 20.4695 3.23754 19.05C2.63476 17.6306 2.33337 16.1139 2.33337 14.5C2.33337 12.8861 2.63476 11.3695 3.23754 9.95001C3.85976 8.53057 4.69587 7.29584 5.74587 6.24584C6.79587 5.19584 8.0306 4.36945 9.45004 3.76668C10.8695 3.14445 12.3862 2.83334 14 2.83334C15.6139 2.83334 17.1306 3.14445 18.55 3.76668C19.9695 4.36945 21.2042 5.19584 22.2542 6.24584C23.3042 7.29584 24.1306 8.53057 24.7334 9.95001C25.3556 11.3695 25.6667 12.8861 25.6667 14.5C25.6667 16.1139 25.3556 17.6306 24.7334 19.05C24.1306 20.4695 23.3042 21.7042 22.2542 22.7542C21.2042 23.8042 19.9695 24.6403 18.55 25.2625C17.1306 25.8653 15.6139 26.1667 14 26.1667ZM14 23.8333C16.6056 23.8333 18.8125 22.9292 20.6209 21.1208C22.4292 19.3125 23.3334 17.1056 23.3334 14.5C23.3334 11.8945 22.4292 9.68751 20.6209 7.87918C18.8125 6.07084 16.6056 5.16668 14 5.16668C11.3945 5.16668 9.18754 6.07084 7.37921 7.87918C5.57087 9.68751 4.66671 11.8945 4.66671 14.5C4.66671 17.1056 5.57087 19.3125 7.37921 21.1208C9.18754 22.9292 11.3945 23.8333 14 23.8333Z" fill="#7D7983"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,21 @@
<svg width="91" height="90" viewBox="0 0 91 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3_6)">
<circle cx="45.5" cy="45" r="27" fill="#3788D6"/>
</g>
<path d="M45.5 26V64" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M65.5 45L27.5 45" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M45.5 26V64" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M65.5 45L27.5 45" stroke="white" stroke-width="2" stroke-linecap="round"/>
<defs>
<filter id="filter0_d_3_6" x="0.6" y="0.1" width="89.8" height="89.8" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="8.95"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.310533 0 0 0 0 0.630263 0 0 0 0 0.938151 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3_6"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3_6" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15L7 10H17L12 15Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 152 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 14L12 9L17 14H7Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 150 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.06667 10.8L11.7667 6.1L10.8333 5.16667L7.06667 8.93333L5.16667 7.03333L4.23333 7.96667L7.06667 10.8ZM3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667V3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13056 2.96667 2 3.33333 2H12.6667C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333V12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14H3.33333Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 596 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 21C6.45 21 5.97917 20.8042 5.5875 20.4125C5.19583 20.0208 5 19.55 5 19V6H4V4H9V3H15V4H20V6H19V19C19 19.55 18.8042 20.0208 18.4125 20.4125C18.0208 20.8042 17.55 21 17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 21C6.45 21 5.97917 20.8042 5.5875 20.4125C5.19583 20.0208 5 19.55 5 19V6H4V4H9V3H15V4H20V6H19V19C19 19.55 18.8042 20.0208 18.4125 20.4125C18.0208 20.8042 17.55 21 17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17Z" fill="#EC221F"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View file

@ -0,0 +1,10 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_9_43)">
<path d="M21.8986 6.46666V21.9681H6.39711V6.46666H21.8986ZM21.8986 4.25217H6.39711C5.17914 4.25217 4.18262 5.24869 4.18262 6.46666V21.9681C4.18262 23.1861 5.17914 24.1826 6.39711 24.1826H21.8986C23.1165 24.1826 24.1131 23.1861 24.1131 21.9681V6.46666C24.1131 5.24869 23.1165 4.25217 21.8986 4.25217Z" fill="#5C5C5C"/>
</g>
<defs>
<clipPath id="clip0_9_43">
<rect width="26.5739" height="26.5739" fill="white" transform="translate(0.86087 0.930435)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 607 B

View file

@ -0,0 +1,3 @@
<svg width="52" height="52" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle opacity="0.05" cx="26" cy="26" r="26" fill="#222222"/>
</svg>

After

Width:  |  Height:  |  Size: 166 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00004 11.3333C8.92226 11.3333 9.70837 11.0083 10.3584 10.3583C11.0084 9.70834 11.3334 8.92223 11.3334 8.00001C11.3334 7.07779 11.0084 6.29168 10.3584 5.64168C9.70837 4.99168 8.92226 4.66668 8.00004 4.66668C7.07782 4.66668 6.29171 4.99168 5.64171 5.64168C4.99171 6.29168 4.66671 7.07779 4.66671 8.00001C4.66671 8.92223 4.99171 9.70834 5.64171 10.3583C6.29171 11.0083 7.07782 11.3333 8.00004 11.3333ZM8.00004 14.6667C7.07782 14.6667 6.21115 14.4917 5.40004 14.1417C4.58893 13.7917 3.88337 13.3167 3.28337 12.7167C2.68337 12.1167 2.20837 11.4111 1.85837 10.6C1.50837 9.7889 1.33337 8.92223 1.33337 8.00001C1.33337 7.07779 1.50837 6.21112 1.85837 5.40001C2.20837 4.5889 2.68337 3.88334 3.28337 3.28334C3.88337 2.68334 4.58893 2.20834 5.40004 1.85834C6.21115 1.50834 7.07782 1.33334 8.00004 1.33334C8.92226 1.33334 9.78893 1.50834 10.6 1.85834C11.4112 2.20834 12.1167 2.68334 12.7167 3.28334C13.3167 3.88334 13.7917 4.5889 14.1417 5.40001C14.4917 6.21112 14.6667 7.07779 14.6667 8.00001C14.6667 8.92223 14.4917 9.7889 14.1417 10.6C13.7917 11.4111 13.3167 12.1167 12.7167 12.7167C12.1167 13.3167 11.4112 13.7917 10.6 14.1417C9.78893 14.4917 8.92226 14.6667 8.00004 14.6667ZM8.00004 13.3333C9.48893 13.3333 10.75 12.8167 11.7834 11.7833C12.8167 10.75 13.3334 9.4889 13.3334 8.00001C13.3334 6.51112 12.8167 5.25001 11.7834 4.21668C10.75 3.18334 9.48893 2.66668 8.00004 2.66668C6.51115 2.66668 5.25004 3.18334 4.21671 4.21668C3.18337 5.25001 2.66671 6.51112 2.66671 8.00001C2.66671 9.4889 3.18337 10.75 4.21671 11.7833C5.25004 12.8167 6.51115 13.3333 8.00004 13.3333Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.99999 18.3333C8.84721 18.3333 7.76388 18.1146 6.74999 17.6771C5.7361 17.2396 4.85416 16.6458 4.10416 15.8958C3.35416 15.1458 2.76041 14.2639 2.32291 13.25C1.88541 12.2361 1.66666 11.1528 1.66666 9.99999C1.66666 8.84721 1.88541 7.76388 2.32291 6.74999C2.76041 5.7361 3.35416 4.85416 4.10416 4.10416C4.85416 3.35416 5.7361 2.76041 6.74999 2.32291C7.76388 1.88541 8.84721 1.66666 9.99999 1.66666C11.1528 1.66666 12.2361 1.88541 13.25 2.32291C14.2639 2.76041 15.1458 3.35416 15.8958 4.10416C16.6458 4.85416 17.2396 5.7361 17.6771 6.74999C18.1146 7.76388 18.3333 8.84721 18.3333 9.99999C18.3333 11.1528 18.1146 12.2361 17.6771 13.25C17.2396 14.2639 16.6458 15.1458 15.8958 15.8958C15.1458 16.6458 14.2639 17.2396 13.25 17.6771C12.2361 18.1146 11.1528 18.3333 9.99999 18.3333ZM9.99999 16.6667C11.8611 16.6667 13.4375 16.0208 14.7292 14.7292C16.0208 13.4375 16.6667 11.8611 16.6667 9.99999C16.6667 8.13888 16.0208 6.56249 14.7292 5.27082C13.4375 3.97916 11.8611 3.33332 9.99999 3.33332C8.13888 3.33332 6.56249 3.97916 5.27082 5.27082C3.97916 6.56249 3.33332 8.13888 3.33332 9.99999C3.33332 11.8611 3.97916 13.4375 5.27082 14.7292C6.56249 16.0208 8.13888 16.6667 9.99999 16.6667Z" fill="#1D1B20"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,42 +1,3 @@
#root { #root {
max-width: 1280px; width: 100%;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
} }

View file

@ -1,35 +1,12 @@
import { useState } from 'react' import React from 'react';
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css' import './App.css'
import Questions from './pages/Questions.tsx'
function App() { const App: React.FC = () => {
const [count, setCount] = useState(0) return (
<Questions />
return ( )
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
} }
export default App export default App

View file

@ -0,0 +1,24 @@
/*Account.module.css*/
.account {
background-color: #F3F3F3;
border-radius: 40px;
align-items: center;
padding: 4.58px 13px 4.58px 4.58px;
margin: 26px 33px 27px 0;
margin-left: auto;
}
.accountText{
font-size: 24px;
font-weight: 600;
color: black;
width: 100%;
text-decoration: none;
}
.accountImg{
vertical-align: middle;
width: 55px;
margin-right: 9px;
}

View file

@ -0,0 +1,20 @@
import React from 'react';
import styles from './Account.module.css'
interface AccountProps {
href: string;
user: string;
}
const Account: React.FC<AccountProps> = ({href, user}) => {
return (
<div className={styles.account}>
<a className={styles.accountText} href={href}>
<img src='../../../public/account.svg' className={styles.accountImg} alt='account'/>
{user}
</a>
</div>
);
};
export default Account;

View file

@ -0,0 +1,17 @@
/*AddAnswerButton.module.css*/
.answerButton {
margin-top: 18px;
display: flex;
gap: 10px;
align-items: center;
border: none;
background-color: white;
color: #3788D6;
font-size: 18px;
font-weight: 500;
}
.addAnswerImg{
vertical-align: middle;
}

View file

@ -0,0 +1,18 @@
import React from "react";
import styles from './AddAnswerButton.module.css'
interface AddAnswerButtonProps {
onClick(): void;
}
const AddAnswerButton: React.FC<AddAnswerButtonProps> = ({onClick}) => {
return (
<button className={styles.answerButton} onClick={onClick}>
Добавить вариант ответа {' '}
<img src='../../../public/add_answer.svg' className={styles.addAnswerImg} alt="add answer"/>
</button>
);
};
export default AddAnswerButton;

View file

@ -0,0 +1,24 @@
/*AddQuestionButton.module.css*/
.questionButton{
display: block;
margin: 0 auto;
display: flex;
flex-direction: column;
margin-bottom: 80px;
align-items: center;
background-color: #F6F6F6;
border: none;
outline: none;
}
.questionButtonImg{
width: 54px;
align-items: center;
}
.textButton{
font-size: 24px;
font-weight: 600;
text-align: center;
}

View file

@ -0,0 +1,17 @@
import React from "react";
import styles from './AddQuestionButton.module.css'
interface AddQuestionButtonProps {
onClick: () => void;
}
const AddQuestionButton: React.FC<AddQuestionButtonProps> = ({onClick}) => {
return (
<button className={styles.questionButton} onClick={onClick}>
<img src='../../../public/add_question.svg' className={styles.questionButtonImg} alt='add question' />
<span className={styles.textButton}>Добавить вопрос</span>
</button>
);
};
export default AddQuestionButton;

View file

@ -0,0 +1,48 @@
/*AnswerOption.module.css*/
.answer{
width: 100%;
display: flex;
gap: 10px;
margin-bottom: 17px;
}
.textAnswer{
text-align: left;
border: none;
background-color: #ffffff;
font-size: 18px;
font-weight: 500;
word-break: break-word;
width: 70%;
}
.buttonMarker{
padding: 0;
border: none;
background-color: transparent;
}
.answerIcon{
vertical-align: middle;
width: 24px;
}
.answerInput{
vertical-align: middle;
font-size: 18px;
font-weight: 500;
outline: none;
border: none;
resize: none;
display: flex;
align-items: center;
box-sizing: border-box;
}
.deleteButton{
margin-left: auto;
border: none;
background-color: transparent;
padding: 0;
}

View file

@ -0,0 +1,97 @@
import React, {useState, useRef, useEffect} from "react";
import styles from'./AnswerOption.module.css';
const single_selected_response = '../../../public/radio_button_checked.svg';
const multiple_selected_response = '../../../public/check_box.svg';
const single_response = '../../../public/radio_button_unchecked.svg';
const multiple_response ='../../../public/emptyCheckbox.svg';
interface AnswerOptionProps{
index: number;
value: string;
onChange: (value: string) => void;
onDelete:(index: number) => void;
selectedType: 'single' | 'multiply';
isSelected: boolean;
toggleSelect: () => void;
}
const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDelete, selectedType, isSelected, toggleSelect}) => {
const [currentValue, setCurrentValue] = useState(value);
const [isEditing, setIsEditing] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
setCurrentValue(value);
}, [value]);
const handleSpanClick = () => {
setIsEditing(true);
}
const handleTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setCurrentValue(event.target.value);
};
const handleSave = () => {
setIsEditing(false);
onChange(currentValue);
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === "Enter") {
event.preventDefault();
handleSave();
}
};
const handleBlur = () => {
handleSave();
};
useEffect(() => {
if (isEditing && textAreaRef.current) {
textAreaRef.current.focus();
}
}, [isEditing]);
const getImage = () => {
if (selectedType === 'multiply') {
return isSelected ? multiple_selected_response : multiple_response;
} else {
return isSelected ? single_selected_response : single_response;
}
};
return (
<div className={styles.answer}>
<button className={styles.buttonMarker} onClick={toggleSelect}>
<img
className={styles.answerIcon}
src={getImage()}
alt=""
/>
</button>
{isEditing ? (
<textarea className={styles.answerInput}
ref={textAreaRef}
value={currentValue}
onChange={handleTextareaChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
placeholder={`Ответ ${index}`}
/>
) : (
<button className={styles.textAnswer} onClick={handleSpanClick}>
{currentValue || `Ответ ${index}`}
</button>
)}
<button className={styles.deleteButton} onClick={() => onDelete(index)}>
<img src='../../../public/delete.svg' alt="Удалить" />
</button>
</div>
);
};
export default AnswerOption;

View file

@ -0,0 +1,8 @@
/*Header.module.css*/
.header{
margin: 0;
padding: 0;
width: 100%;
display: flex;
}

View file

@ -0,0 +1,29 @@
import React, {useState} from "react";
import Logo from "../Logo/Logo.tsx";
import Account from "../Account/Account.tsx";
import styles from './Header.module.css'
import SurveyPagesList from "../SurveyPagesList/SurveyPagesList.tsx";
const Header: React.FC = () => {
const [activePage, setActivePage] = useState('Создать опрос');
const handlePageClick = (name: string)=> {
setActivePage(name);
}
return (
<div className={styles.header}>
<Logo href='' />
<SurveyPagesList
activePage={activePage}
onPageClick = {handlePageClick}
/>
<Account
href=''
user='Иванов Иван'
/>
</div>
);
};
export default Header;

View file

@ -0,0 +1,8 @@
/*Logo.module.css*/
.logo {
padding: 0;
height: 52px;
width: 52px;
margin: 31px 77px 25px 40px;
}

View file

@ -0,0 +1,16 @@
import React from "react";
import styles from './Logo.module.css'
interface LogoProps {
href: string;
}
const Logo: React.FC<LogoProps> = ({href}) => {
return (
<a className={styles.logo} href={href}>
<img src='../../../public/logo.svg' alt="" />
</a>
);
};
export default Logo;

View file

@ -0,0 +1,8 @@
/*MainComponent.module.css*/
.mainPage{
width: 100%;
min-height: 85vh;
display: flex;
background-color: #F6F6F6;
}

View file

@ -0,0 +1,29 @@
import Navigation from "../Navigation/Navigation.tsx";
import React, {useState} from "react";
import styles from './MainComponent.module.css'
import Survey from "../Survey/Survey.tsx";
import SettingSurvey from "../SettingSurvey/SettingSurvey.tsx";
const MainComponent: React.FC = () => {
const [activePage, setActivePage] = useState(
localStorage.getItem("activePage") || "Вопросы"
);
const handleNavigationClick = (title: string) => {
setActivePage(title);
localStorage.setItem('activePage', title);
}
return (
<main className={styles.mainPage}>
<Navigation
activePage={activePage}
onNavigationClick={handleNavigationClick}
/>
{ activePage === 'Вопросы' && <Survey />}
{activePage === 'Настройки' && <SettingSurvey />}
</main>
)
}
export default MainComponent;

View file

@ -0,0 +1,17 @@
/*Navigation.module.css*/
.navContainer{
display: flex;
flex-direction: column;
}
.nav{
margin: 34px 0 48px 40px;
background-color: white;
border-radius: 20px;
}
.navList{
list-style: none;
padding: 52px 57px 70px 36px;
}

View file

@ -0,0 +1,34 @@
import React from 'react'
import styles from './Navigation.module.css'
import NavigationItem from "../NavigationItem/NavigationItem.tsx";
import SaveButton from "../SaveButton/SaveButton.tsx";
interface NavigationProps {
onNavigationClick: (title: string) => void;
activePage: string
}
const Navigation: React.FC<NavigationProps> = ({onNavigationClick, activePage}) => {
const items: string[] = ['Вопросы', 'Настройки', 'Результаты']
return (
<div className={styles.navContainer}>
<nav className={styles.nav}>
<ul className={styles.navList}>
{items.map(item => (
<NavigationItem
key={item}
title={item}
isActive={activePage === item}
onClick={() => onNavigationClick(item)}
/>
))}
</ul>
</nav>
<SaveButton />
</div>
);
};
export default Navigation;

View file

@ -0,0 +1,23 @@
/*NavigationItem.module.css*/
.navItem{
padding: 0;
margin-bottom: 42px;
}
.page{
background-color: white;
border: none;
font-size: 24px;
font-weight: 600;
color: #AFAFAF;
}
.active{
text-decoration: underline 2px #556FB7;
color: #000000;
}
.navItem:last-child{
margin-bottom: 0;
}

View file

@ -0,0 +1,20 @@
import React from 'react'
import styles from './NavigationItem.module.css'
interface NavigationItemProps{
title: string;
onClick(): void;
isActive: boolean;
}
const NavigationItem: React.FC<NavigationItemProps> = ({title, onClick, isActive}) => {
return (
<li className={styles.navItem}>
<button className={`${styles.page} ${isActive ? styles.active : ''}`} onClick={onClick}>
{title}
</button>
</li>
);
};
export default NavigationItem;

View file

@ -0,0 +1,22 @@
/*PageSurvey.module.css*/
.pagesSurveyItem{
align-items: center;
}
.pageSurvey{
font-size: 24px;
font-weight: 600;
color: #2A6DAE;
padding: 0;
border: none;
background-color: #ffffff;
padding-bottom: 5px;
white-space: nowrap;
}
.active{
color: #000000;
text-decoration: underline;
text-decoration-color: #3881C8;
}

View file

@ -0,0 +1,20 @@
import React from 'react';
import styles from './PageSurvey.module.css';
interface PageSurveyProps{
name: string;
isActive: boolean;
onClick(): void;
}
const PageSurvey: React.FC<PageSurveyProps> = ({name, isActive, onClick}) => {
return (
<li className={styles.pagesSurveyItem}>
<button className={`${styles.pageSurvey} ${isActive ? styles.active : ''}`} onClick={onClick}>
{name}
</button>
</li>
);
};
export default PageSurvey;

View file

@ -0,0 +1,74 @@
/*QuestionItem.module.css*/
.questionCard{
background-color: white;
display: flex;
justify-content: space-between;
margin-bottom: 34px;
padding: 27px 29px 26px 36px;
border-radius: 14px;
}
.questionContainer{
width: 100%;
display: flex;
flex-direction: column;
}
.question{
display: flex;
justify-content: space-between;
gap: 40px;
}
.questionTextarea{
width: 70%;
align-items: center;
border: none;
outline: none;
resize: none;
margin-bottom: 5px;
font-size: 24px;
font-weight: 600;
}
.buttonQuestion{
align-items: center;
border: none;
outline: none;
background-color: #ffffff;
padding: 0;
}
.textQuestion{
margin-top: 0;
width: 100%;
font-size: 24px;
font-weight: 600;
margin-bottom: 35px;
text-align: start;
word-break: break-word;
}
.questionActions{
display: flex;
justify-content: space-between;
}
.deleteQuestionButton{
font-size: 18px;
font-weight: 500;
color: #EC221F;
border: none;
padding: 0;
align-items: center;
background-color: transparent;
display: flex;
gap: 3px;
}
.basketImg{
vertical-align: middle;
width: 24px;
color: #EC221F;
}

Some files were not shown because too many files have changed in this diff Show more