Admin controller, Locations controller, requests to add available countries, request to get available countries

This commit is contained in:
2024-08-17 21:30:51 +03:00
parent 7cbe3d9698
commit 3cb2083222
59 changed files with 477 additions and 167 deletions

View File

@@ -1,4 +0,0 @@
namespace ApplicationLayer.AuthServices.LoginService.Exceptions
{
public class IncorrectLoginDataException() : Exception("Incorrect email or password");
}

View File

@@ -1,6 +0,0 @@
using ApplicationLayer.AuthServices.Requests;
namespace ApplicationLayer.AuthServices.RegisterService.Exceptions
{
public class UserAlreadyExistsException(RegisterApplicantRequest request) : Exception($"User with email '{request.Email}' already exists");
}

View File

@@ -1,31 +0,0 @@
using ApplicationLayer.AuthServices.NeededServices;
using ApplicationLayer.AuthServices.RegisterService.Exceptions;
using ApplicationLayer.AuthServices.Requests;
using Domains.Users;
namespace ApplicationLayer.AuthServices.RegisterService
{
/// <inheritdoc cref="IRegisterService"/>
public class RegisterService(IUsersRepository users) : IRegisterService
{
async Task IRegisterService.Register(RegisterApplicantRequest request, CancellationToken cancellationToken)
{
if (await users.FindByEmailAsync(request.Email, cancellationToken) is not null)
{
throw new UserAlreadyExistsException(request);
}
//TODO mapper
var user = new User
{
Email = request.Email,
Password = request.Password,
Role = Role.Applicant
};
await users.AddAsync(user, cancellationToken);
await users.SaveAsync(cancellationToken);
users.GetAllAsync(cancellationToken);
}
}
}

View File

@@ -1,4 +0,0 @@
namespace ApplicationLayer.AuthServices.Requests
{
public record RegisterApplicantRequest(string Email, string Password);
}

View File

@@ -1,4 +0,0 @@
namespace ApplicationLayer.AuthServices.Requests
{
public record UserLoginRequest(string Email, string Password);
}

View File

@@ -1,4 +1,4 @@
namespace ApplicationLayer.DataAccessingServices.VisaApplications.Models;
namespace ApplicationLayer.DataAccessingServices.Applicants.Models;
public class AddressModel
{

View File

@@ -1,4 +1,4 @@
namespace ApplicationLayer.DataAccessingServices.VisaApplications.Models;
namespace ApplicationLayer.DataAccessingServices.Applicants.Models;
public class PlaceOfWorkModel
{

View File

@@ -4,4 +4,8 @@ using Domains.ApplicantDomain;
namespace ApplicationLayer.DataAccessingServices.Applicants.NeededServices;
/// Repository pattern for <see cref="Applicant"/>
public interface IApplicantsRepository : IGenericRepository<Applicant>;
public interface IApplicantsRepository : IGenericRepository<Applicant>
{
/// Find <see cref="Applicant"/> by Identifier
Task<Applicant> FindByUserIdAsync(Guid userId, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,28 @@
using ApplicationLayer.DataAccessingServices.AuthServices.LoginService.Exceptions;
using ApplicationLayer.DataAccessingServices.AuthServices.NeededServices;
using ApplicationLayer.DataAccessingServices.AuthServices.Requests;
using Domains.Users;
namespace ApplicationLayer.DataAccessingServices.AuthServices.LoginService
{
public class DevelopmentLoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService
{
async Task<string> ILoginService.LoginAsync(UserLoginRequest request, CancellationToken cancellationToken)
{
if (request is { Email: "admin@mail.ru", Password: "admin" })
{
var admin = new User { Role = Role.Admin };
return tokenGenerator.CreateToken(admin);
}
var user = await users.FindByEmailAsync(request.Email, cancellationToken);
if (user is null || user.Password != request.Password)
{
throw new IncorrectLoginDataException();
}
return tokenGenerator.CreateToken(user);
}
}
}

View File

@@ -0,0 +1,6 @@
using ApplicationLayer.GeneralExceptions;
namespace ApplicationLayer.DataAccessingServices.AuthServices.LoginService.Exceptions
{
public class IncorrectLoginDataException() : ApiException("Incorrect email or password");
}

View File

@@ -1,6 +1,6 @@
using ApplicationLayer.AuthServices.Requests;
using ApplicationLayer.DataAccessingServices.AuthServices.Requests;
namespace ApplicationLayer.AuthServices.LoginService
namespace ApplicationLayer.DataAccessingServices.AuthServices.LoginService
{
/// Handles <see cref="UserLoginRequest"/>
public interface ILoginService

View File

@@ -1,8 +1,8 @@
using ApplicationLayer.AuthServices.LoginService.Exceptions;
using ApplicationLayer.AuthServices.NeededServices;
using ApplicationLayer.AuthServices.Requests;
using ApplicationLayer.DataAccessingServices.AuthServices.LoginService.Exceptions;
using ApplicationLayer.DataAccessingServices.AuthServices.NeededServices;
using ApplicationLayer.DataAccessingServices.AuthServices.Requests;
namespace ApplicationLayer.AuthServices.LoginService
namespace ApplicationLayer.DataAccessingServices.AuthServices.LoginService
{
/// <inheritdoc cref="ILoginService"/>
public class LoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService

View File

@@ -1,6 +1,6 @@
using Domains.Users;
namespace ApplicationLayer.AuthServices.NeededServices
namespace ApplicationLayer.DataAccessingServices.AuthServices.NeededServices
{
public interface ITokenGenerator
{

View File

@@ -1,7 +1,7 @@
using ApplicationLayer.GeneralNeededServices;
using Domains.Users;
namespace ApplicationLayer.AuthServices.NeededServices
namespace ApplicationLayer.DataAccessingServices.AuthServices.NeededServices
{
/// Repository pattern for <see cref="User"/>
public interface IUsersRepository : IGenericRepository<User>

View File

@@ -0,0 +1,7 @@
using ApplicationLayer.DataAccessingServices.AuthServices.Requests;
using ApplicationLayer.GeneralExceptions;
namespace ApplicationLayer.DataAccessingServices.AuthServices.RegisterService.Exceptions
{
public class UserAlreadyExistsException(RegisterApplicantRequest request) : AlreadyExistsException($"User with email '{request.Email}' already exists");
}

View File

@@ -1,6 +1,6 @@
using ApplicationLayer.AuthServices.Requests;
using ApplicationLayer.DataAccessingServices.AuthServices.Requests;
namespace ApplicationLayer.AuthServices.RegisterService
namespace ApplicationLayer.DataAccessingServices.AuthServices.RegisterService
{
/// Handles <see cref="RegisterApplicantRequest"/>
public interface IRegisterService

View File

@@ -0,0 +1,71 @@
using ApplicationLayer.DataAccessingServices.Applicants.NeededServices;
using ApplicationLayer.DataAccessingServices.AuthServices.NeededServices;
using ApplicationLayer.DataAccessingServices.AuthServices.RegisterService.Exceptions;
using ApplicationLayer.DataAccessingServices.AuthServices.Requests;
using ApplicationLayer.DataAccessingServices.Locations.NeededServices;
using ApplicationLayer.GeneralNeededServices;
using Domains.ApplicantDomain;
using Domains.Users;
namespace ApplicationLayer.DataAccessingServices.AuthServices.RegisterService
{
/// <inheritdoc cref="IRegisterService"/>
public class RegisterService(
IUsersRepository users,
IApplicantsRepository applicants,
ICitiesRepository cities,
IUnitOfWork unitOfWork) : IRegisterService
{
async Task IRegisterService.Register(RegisterApplicantRequest request, CancellationToken cancellationToken)
{
if (await users.FindByEmailAsync(request.Email, cancellationToken) is not null)
{
throw new UserAlreadyExistsException(request);
}
//TODO mapper
var user = new User { Email = request.Email, Password = request.Password, Role = Role.Applicant };
var applicantCity = await cities.GetByIdAsync(request.CityOfBirthId, cancellationToken);
var placeOfWorkCity = await cities.GetByIdAsync(request.PlaceOfWork.Address.CityId, cancellationToken);
var placeOfWorkAddress = new Address
{
Country = placeOfWorkCity.Country,
City = placeOfWorkCity,
Building = request.PlaceOfWork.Address.Building,
Street = request.PlaceOfWork.Address.Street
};
var placeOfWork = new PlaceOfWork
{
Name = request.PlaceOfWork.Name,
Address = placeOfWorkAddress,
PhoneNum = request.PlaceOfWork.PhoneNum
};
var applicant = new Applicant
{
Citizenship = request.Citizenship,
CitizenshipByBirth = request.CitizenshipByBirth,
Gender = request.Gender,
Name = request.ApplicantName,
Passport = request.Passport,
BirthDate = request.BirthDate,
FatherName = request.FatherName,
JobTitle = request.JobTitle,
MaritalStatus = request.MaritalStatus,
MotherName = request.MotherName,
UserId = user.Id,
CityOfBirth = applicantCity,
CountryOfBirth = applicantCity.Country,
IsNonResident = request.IsNonResident,
PlaceOfWork = placeOfWork
};
await users.AddAsync(user, cancellationToken);
await applicants.AddAsync(applicant, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
}
}
}

View File

@@ -0,0 +1,22 @@
using ApplicationLayer.DataAccessingServices.Applicants.Models;
using Domains.ApplicantDomain;
namespace ApplicationLayer.DataAccessingServices.AuthServices.Requests
{
public record RegisterApplicantRequest(
string Email,
string Password,
Name ApplicantName,
Passport Passport,
DateTime BirthDate,
Guid CityOfBirthId,
string Citizenship,
string CitizenshipByBirth,
Gender Gender,
MaritalStatus MaritalStatus,
Name FatherName,
Name MotherName,
string JobTitle,
PlaceOfWorkModel PlaceOfWork,
bool IsNonResident);
}

View File

@@ -0,0 +1,4 @@
namespace ApplicationLayer.DataAccessingServices.AuthServices.Requests
{
public record UserLoginRequest(string Email, string Password);
}

View File

@@ -3,4 +3,11 @@ using Domains.LocationDomain;
namespace ApplicationLayer.DataAccessingServices.Locations.NeededServices;
public interface ICountriesRepository : IGenericRepository<Country>;
public interface ICountriesRepository : IGenericRepository<Country>
{
/// Gets country by name
/// <param name="countryName">Name of country to seek</param>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Country or null if not found</returns>
Task<Country?> FindByName(string countryName, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,36 @@
using ApplicationLayer.DataAccessingServices.Locations.NeededServices;
using ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.AdminRequests.Exceptions;
using ApplicationLayer.DataAccessingServices.Locations.Requests;
using ApplicationLayer.GeneralNeededServices;
using Domains.LocationDomain;
namespace ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.AdminRequests
{
/// <inheritdoc cref="IEditLocationsRequestsHandler"/>
public class EditLocationsRequestsHandler(ICountriesRepository countries, IUnitOfWork unitOfWork) : IEditLocationsRequestsHandler
{
async Task IEditLocationsRequestsHandler.AddCountryAsync(AddCountryRequest request, CancellationToken cancellationToken)
{
if (await countries.FindByName(request.CountryName, cancellationToken) is not null)
{
throw new CountryAlreadyExists(request.CountryName);
}
if (request.Cities.Distinct().Count() < request.Cities.Length)
{
throw new MultipleIdenticalCitiesInCountry();
}
var country = new Country
{
Name = request.CountryName,
IsSchengen = request.IsSchengen,
Cities = request.Cities.Select(cityName => new City { Name = cityName }).ToList()
};
await countries.AddAsync(country, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
}
}
}

View File

@@ -0,0 +1,6 @@
using ApplicationLayer.GeneralExceptions;
namespace ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.AdminRequests.Exceptions
{
public class CountryAlreadyExists(string countryName) : AlreadyExistsException($"{countryName} already exists.");
}

View File

@@ -0,0 +1,6 @@
using ApplicationLayer.GeneralExceptions;
namespace ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.AdminRequests.Exceptions
{
public class MultipleIdenticalCitiesInCountry() : ApiException("There are multiple cities with one name in the country.");
}

View File

@@ -0,0 +1,11 @@
using ApplicationLayer.DataAccessingServices.Locations.Requests;
namespace ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.AdminRequests
{
/// Handles edit requests of locations from admins
public interface IEditLocationsRequestsHandler
{
/// Handles add country requests
Task AddCountryAsync(AddCountryRequest request, CancellationToken cancellationToken);
}
}

View File

@@ -0,0 +1,12 @@
using Domains.LocationDomain;
namespace ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.ApplicantRequests
{
/// Handles location requests
public interface ILocationRequestsHandler
{
/// Handle get request
/// <returns>List of available countries</returns>
Task<List<Country>> HandleGetRequestAsync(CancellationToken cancellationToken);
}
}

View File

@@ -0,0 +1,14 @@
using ApplicationLayer.DataAccessingServices.Locations.NeededServices;
using Domains.LocationDomain;
namespace ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.ApplicantRequests
{
/// <inheritdoc cref="ILocationRequestsHandler"/>
public class LocationRequestsHandler(ICountriesRepository countries) : ILocationRequestsHandler
{
async Task<List<Country>> ILocationRequestsHandler.HandleGetRequestAsync(CancellationToken cancellationToken)
{
return await countries.GetAllAsync(cancellationToken);
}
}
}

View File

@@ -0,0 +1,4 @@
namespace ApplicationLayer.DataAccessingServices.Locations.Requests
{
public record AddCountryRequest(string CountryName, bool IsSchengen, string[] Cities);
}

View File

@@ -3,9 +3,9 @@ using Domains.VisaApplicationDomain;
namespace ApplicationLayer.DataAccessingServices.VisaApplications.Handlers;
public interface IVisaApplicationsRequestHandler
public interface IVisaApplicationRequestsHandler
{
Task<List<VisaApplication>> Get(CancellationToken cancellationToken);
void HandleCreateRequest(VisaApplicationCreateRequest request, CancellationToken cancellationToken);
void HandleCreateRequest(Guid userId, VisaApplicationCreateRequest request, CancellationToken cancellationToken);
}

View File

@@ -1,8 +1,9 @@
using ApplicationLayer.DataAccessingServices.Locations.NeededServices;
using ApplicationLayer.DataAccessingServices.Applicants.NeededServices;
using ApplicationLayer.DataAccessingServices.Locations.NeededServices;
using ApplicationLayer.DataAccessingServices.VisaApplications.Models;
using ApplicationLayer.DataAccessingServices.VisaApplications.NeededServices;
using ApplicationLayer.DataAccessingServices.VisaApplications.Requests;
using Domains.ApplicantDomain;
using ApplicationLayer.GeneralNeededServices;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.DataAccessingServices.VisaApplications.Handlers;
@@ -10,42 +11,18 @@ namespace ApplicationLayer.DataAccessingServices.VisaApplications.Handlers;
/// Handles visa requests
public class VisaApplicationRequestsHandler(
IVisaApplicationsRepository applications,
ICitiesRepository cities,
ICountriesRepository countries) : IVisaApplicationsRequestHandler
IApplicantsRepository applicants,
ICountriesRepository countries,
IUnitOfWork unitOfWork) : IVisaApplicationRequestsHandler
{
public async Task<List<VisaApplication>> Get(CancellationToken cancellationToken) => await applications.GetAllAsync(cancellationToken);
public async void HandleCreateRequest(VisaApplicationCreateRequest request, CancellationToken cancellationToken)
public async void HandleCreateRequest(Guid userId, VisaApplicationCreateRequest request, CancellationToken cancellationToken)
{
//TODO fix
//TODO mapper
var cityOfBirth = await cities.GetByIdAsync(request.BirthCityId, cancellationToken);
var cityOfWork = await cities.GetByIdAsync(request.PlaceOfWork.Address.CityId, cancellationToken);
var addressOfWork = new Address
{
City = cityOfWork,
Country = cityOfWork.Country,
Building = request.PlaceOfWork.Address.Building,
Street = request.PlaceOfWork.Address.Street
};
var applicant = new Applicant
{
MaritalStatus = request.MaritalStatus,
Name = request.FullName,
Passport = request.Passport,
Gender = request.Gender,
Citizenship = request.CitizenShip,
BirthDate = request.BirthDate,
FatherName = request.FatherName,
JobTitle = request.JobTitle,
MotherName = request.MotherName,
CitizenshipByBirth = request.CitizenshipByBirth,
CityOfBirth = cityOfBirth,
CountryOfBirth = cityOfBirth.Country,
IsNonResident = request.IsNonResident,
PlaceOfWork = new PlaceOfWork { Address = addressOfWork, Name = request.PlaceOfWork.Name, PhoneNum = request.PlaceOfWork.PhoneNum }
};
var applicant = await applicants.FindByUserIdAsync(userId, cancellationToken);
var pastVisits = request.PastVisits.Select(m => ConvertPastVisitModelToPastVisit(m, cancellationToken).Result).ToList();
var visaApplication = new VisaApplication
@@ -64,7 +41,8 @@ public class VisaApplicationRequestsHandler(
};
await applications.AddAsync(visaApplication, cancellationToken);
await applications.SaveAsync(cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
}
private async Task<PastVisit> ConvertPastVisitModelToPastVisit(PastVisitModel model, CancellationToken cancellationToken)

View File

@@ -1,25 +1,11 @@
using ApplicationLayer.DataAccessingServices.VisaApplications.Models;
using Domains.ApplicantDomain;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.DataAccessingServices.VisaApplications.Requests;
/// Model of visa request from user
public record VisaApplicationCreateRequest(
Name FullName,
Passport Passport,
DateTime BirthDate,
Guid BirthCityId,
string CitizenShip,
string CitizenshipByBirth,
Gender Gender,
MaritalStatus MaritalStatus,
Name FatherName,
Name MotherName,
bool IsNonResident,
ReentryPermit ReentryPermit,
string JobTitle,
PlaceOfWorkModel PlaceOfWork,
Guid DestinationCountryId,
VisaCategory VisaCategory,
bool IsForGroup,

View File

@@ -1,5 +1,7 @@
using ApplicationLayer.AuthServices.LoginService;
using ApplicationLayer.AuthServices.RegisterService;
using ApplicationLayer.DataAccessingServices.AuthServices.LoginService;
using ApplicationLayer.DataAccessingServices.AuthServices.RegisterService;
using ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.AdminRequests;
using ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.ApplicantRequests;
using ApplicationLayer.DataAccessingServices.VisaApplications.Handlers;
using Microsoft.Extensions.DependencyInjection;
@@ -9,12 +11,22 @@ namespace ApplicationLayer;
public static class DependencyInjection
{
/// Add services for Application layer
public static IServiceCollection AddApplicationLayer(this IServiceCollection services)
public static IServiceCollection AddApplicationLayer(this IServiceCollection services, bool isDevelopment = false)
{
services.AddScoped<IVisaApplicationsRequestHandler, VisaApplicationRequestsHandler>();
services.AddScoped<IVisaApplicationRequestsHandler, VisaApplicationRequestsHandler>();
services.AddScoped<ILocationRequestsHandler, LocationRequestsHandler>();
services.AddScoped<IEditLocationsRequestsHandler, EditLocationsRequestsHandler>();
services.AddScoped<IRegisterService, RegisterService>();
services.AddScoped<ILoginService, LoginService>();
if (isDevelopment)
{
services.AddScoped<ILoginService, DevelopmentLoginService>();
}
else
{
services.AddScoped<ILoginService, LoginService>();
}
return services;
}

View File

@@ -0,0 +1,4 @@
namespace ApplicationLayer.GeneralExceptions
{
public class AlreadyExistsException(string message) : ApiException(message);
}

View File

@@ -0,0 +1,4 @@
namespace ApplicationLayer.GeneralExceptions
{
public class ApiException(string message) : Exception(message);
}

View File

@@ -34,7 +34,4 @@ public interface IGenericRepository<T> where T : class, IEntity
/// </summary>
/// <param name="entity">Entity to remove</param>
void Remove(T entity);
/// Save changes in storage
Task SaveAsync(CancellationToken cancellationToken);
}

View File

@@ -1,8 +1,8 @@
namespace Infrastructure.Database;
namespace ApplicationLayer.GeneralNeededServices;
public interface IUnitOfWork
{
/// Saves changes in data storage
/// <param name="cancellationToken">Cancellation Token</param>
Task SaveAsync(CancellationToken cancellationToken);
}
}

View File

@@ -6,7 +6,9 @@ namespace Domains.ApplicantDomain;
public class Applicant : IEntity
{
/// Unique identifier of the <see cref="Applicant"/>
public Guid Id { get; set; }
public Guid Id { get; private set; } = Guid.NewGuid();
public Guid UserId { get; set; }
/// Full name of the <see cref="Applicant"/>
public Name Name { get; set; } = null!;

View File

@@ -1,4 +1,6 @@
namespace Domains.LocationDomain;
using System.Text.Json.Serialization;
namespace Domains.LocationDomain;
/// Model of a city
public class City : IEntity
@@ -10,5 +12,6 @@ public class City : IEntity
public string Name { get; set; } = null!;
/// <see cref="LocationDomain.Country"/> in which the city is located
[JsonIgnore]
public Country Country { get; set; } = null!;
}
}

View File

@@ -1,5 +1,5 @@
using System.IdentityModel.Tokens.Jwt;
using ApplicationLayer.AuthServices.NeededServices;
using ApplicationLayer.DataAccessingServices.AuthServices.NeededServices;
using ApplicationLayer.GeneralNeededServices;
using Microsoft.Extensions.DependencyInjection;

View File

@@ -1,6 +1,6 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using ApplicationLayer.AuthServices.NeededServices;
using ApplicationLayer.DataAccessingServices.AuthServices.NeededServices;
using ApplicationLayer.GeneralNeededServices;
using Domains.Users;
@@ -14,7 +14,7 @@ namespace Infrastructure.Auth
var claims = new List<Claim>
{
new(ClaimTypes.Role, user.Role.ToString()),
new(ClaimTypes.Email, user.Email)
new(ClaimTypes.NameIdentifier, user.Id.ToString())
};
var token = new JwtSecurityToken(

View File

@@ -1,4 +1,5 @@
using Domains.ApplicantDomain;
using Domains.Users;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
@@ -16,6 +17,7 @@ public class ApplicantConfiguration : IEntityTypeConfiguration<Applicant>
entity.HasOne(a => a.CityOfBirth).WithMany().OnDelete(DeleteBehavior.Restrict);
entity.HasOne(a => a.CountryOfBirth).WithMany().OnDelete(DeleteBehavior.Restrict);
entity.HasOne<User>().WithOne().HasForeignKey<Applicant>(a => a.UserId);
entity.Property(p => p.Citizenship)
.IsUnicode(false)

View File

@@ -1,5 +1,6 @@
using ApplicationLayer.DataAccessingServices.Applicants.NeededServices;
using Domains.ApplicantDomain;
using Infrastructure.Database.Applicants.Repositories.Exceptions;
using Infrastructure.Database.Generic;
using Microsoft.EntityFrameworkCore;
@@ -8,9 +9,8 @@ namespace Infrastructure.Database.Applicants.Repositories;
/// Repository pattern for <see cref="Applicant"/>
/// <param name="reader"><inheritdoc cref="IGenericReader"/></param>
/// <param name="writer"><inheritdoc cref="IGenericWriter"/></param>
/// <param name="unitOfWork"><inheritdoc cref="IUnitOfWork"/></param>
public sealed class ApplicantsRepository(IGenericReader reader, IGenericWriter writer, IUnitOfWork unitOfWork)
: GenericRepository<Applicant>(reader, writer, unitOfWork), IApplicantsRepository
public sealed class ApplicantsRepository(IGenericReader reader, IGenericWriter writer)
: GenericRepository<Applicant>(reader, writer), IApplicantsRepository
{
protected override IQueryable<Applicant> LoadDomain()
{
@@ -19,4 +19,10 @@ public sealed class ApplicantsRepository(IGenericReader reader, IGenericWriter w
.Include(a => a.CityOfBirth)
.Include(a => a.PlaceOfWork);
}
async Task<Applicant> IApplicantsRepository.FindByUserIdAsync(Guid userId, CancellationToken cancellationToken)
{
var result = await LoadDomain().SingleOrDefaultAsync(a => a.UserId == userId, cancellationToken);
return result ?? throw new ApplicantNotFoundByUserIdException(userId);
}
}

View File

@@ -0,0 +1,10 @@
using Domains.ApplicantDomain;
using Infrastructure.Database.GeneralExceptions;
namespace Infrastructure.Database.Applicants.Repositories.Exceptions
{
public class ApplicantNotFoundByUserIdException(Guid id) : EntityNotFoundException<Applicant>("Applicant not found.")
{
public Guid UserId { get; private set; } = id;
}
}

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using ApplicationLayer.GeneralNeededServices;
using Infrastructure.Database.Generic;
using Microsoft.EntityFrameworkCore;

View File

@@ -1,8 +1,9 @@
using Domains;
using ApplicationLayer.GeneralExceptions;
using Domains;
namespace Infrastructure.Database.GeneralExceptions;
/// Exception to throw when entity not found
/// <typeparam name="T">Not found entity type</typeparam>
public class EntityNotFoundException<T>(string message) : Exception(message)
public class EntityNotFoundException<T>(string message) : ApiException(message)
where T : class, IEntity;

View File

@@ -7,10 +7,9 @@ namespace Infrastructure.Database.Generic;
/// Generic repository pattern
/// <param name="writer"><inheritdoc cref="IGenericWriter"/></param>
/// <param name="unitOfWork"><inheritdoc cref="IUnitOfWork"/></param>
/// <typeparam name="T">Type of entity</typeparam>
/// <remarks>Should be inherited to create typed repositories</remarks>
public abstract class GenericRepository<T>(IGenericReader reader, IGenericWriter writer, IUnitOfWork unitOfWork) : IGenericRepository<T>
public abstract class GenericRepository<T>(IGenericReader reader, IGenericWriter writer) : IGenericRepository<T>
where T : class, IEntity
{
/// <inheritdoc cref="IGenericRepository{T}.GetAllAsync"/>
@@ -41,10 +40,6 @@ public abstract class GenericRepository<T>(IGenericReader reader, IGenericWriter
writer.Remove(entity);
}
/// <inheritdoc cref="IGenericRepository{T}.SaveAsync"/>
public async Task SaveAsync(CancellationToken cancellationToken)
=> await unitOfWork.SaveAsync(cancellationToken);
/// Should be overriden to load navigation properties of entity
protected virtual IQueryable<T> LoadDomain()
{

View File

@@ -1,4 +1,5 @@
using Domains;
using ApplicationLayer.GeneralNeededServices;
using Domains;
namespace Infrastructure.Database.Generic;

View File

@@ -8,8 +8,10 @@ public class CountryConfiguration : IEntityTypeConfiguration<Country>
{
public void Configure(EntityTypeBuilder<Country> entity)
{
entity.Property(p => p.Name)
entity.Property(c => c.Name)
.IsUnicode(false)
.HasMaxLength(70);
entity.HasIndex(c => c.Name).IsUnique();
}
}
}

View File

@@ -5,8 +5,8 @@ using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Database.Locations.Repositories.Cities;
public sealed class CitiesRepository(IGenericReader reader, IGenericWriter writer, IUnitOfWork unitOfWork)
: GenericRepository<City>(reader, writer, unitOfWork), ICitiesRepository
public sealed class CitiesRepository(IGenericReader reader, IGenericWriter writer)
: GenericRepository<City>(reader, writer), ICitiesRepository
{
protected override IQueryable<City> LoadDomain()
{

View File

@@ -5,11 +5,17 @@ using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Database.Locations.Repositories.Countries;
public sealed class CountriesRepository(IGenericReader reader, IGenericWriter writer, IUnitOfWork unitOfWork)
: GenericRepository<Country>(reader, writer, unitOfWork), ICountriesRepository
public sealed class CountriesRepository(IGenericReader reader, IGenericWriter writer)
: GenericRepository<Country>(reader, writer), ICountriesRepository
{
protected override IQueryable<Country> LoadDomain()
{
return base.LoadDomain().Include(c => c.Cities);
}
}
async Task<Country?> ICountriesRepository.FindByName(string countryName, CancellationToken cancellationToken)
{
var result = await LoadDomain().SingleOrDefaultAsync(c => c.Name == countryName, cancellationToken);
return result;
}
}

View File

@@ -1,4 +1,4 @@
using ApplicationLayer.AuthServices.NeededServices;
using ApplicationLayer.DataAccessingServices.AuthServices.NeededServices;
using Domains.Users;
using Infrastructure.Database.Generic;
using Microsoft.EntityFrameworkCore;
@@ -6,8 +6,8 @@ using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Database.Users.Repositories
{
/// <inheritdoc cref="IUsersRepository"/>
public class UsersRepository(IGenericReader reader, IGenericWriter writer, IUnitOfWork unitOfWork)
: GenericRepository<User>(reader, writer, unitOfWork), IUsersRepository
public class UsersRepository(IGenericReader reader, IGenericWriter writer)
: GenericRepository<User>(reader, writer), IUsersRepository
{
async Task<User?> IUsersRepository.FindByEmailAsync(string email, CancellationToken cancellationToken)
{

View File

@@ -5,8 +5,8 @@ using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Database.VisaApplications.Repositories;
public sealed class VisaApplicationsRepository(IGenericReader reader, IGenericWriter writer, IUnitOfWork unitOfWork)
: GenericRepository<VisaApplication>(reader, writer, unitOfWork), IVisaApplicationsRepository
public sealed class VisaApplicationsRepository(IGenericReader reader, IGenericWriter writer)
: GenericRepository<VisaApplication>(reader, writer), IVisaApplicationsRepository
{
protected override IQueryable<VisaApplication> LoadDomain()
{
@@ -15,4 +15,4 @@ public sealed class VisaApplicationsRepository(IGenericReader reader, IGenericWr
.Include(a => a.PastVisas)
.Include(a => a.PastVisits);
}
}
}

View File

@@ -1,11 +1,9 @@
using ApplicationLayer.AuthServices.NeededServices;
using ApplicationLayer.DataAccessingServices.Applicants.NeededServices;
using ApplicationLayer.DataAccessingServices.Applicants.NeededServices;
using ApplicationLayer.DataAccessingServices.AuthServices.NeededServices;
using ApplicationLayer.DataAccessingServices.Locations.NeededServices;
using ApplicationLayer.DataAccessingServices.VisaApplications.NeededServices;
using ApplicationLayer.GeneralNeededServices;
using Infrastructure.Auth;
using Infrastructure.Common;
using Infrastructure.Database;
using Infrastructure.Database.Applicants.Repositories;
using Infrastructure.Database.Generic;
using Infrastructure.Database.Locations.Repositories.Cities;

View File

@@ -0,0 +1,37 @@
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace SchengenVisaApi.Common
{
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
void IConfigureOptions<SwaggerGenOptions>.Configure(SwaggerGenOptions options)
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Provide a JWT-token.",
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
}
}
}

View File

@@ -0,0 +1,12 @@
namespace SchengenVisaApi.Common
{
#pragma warning disable CS1591
public static class PolicyConstants
{
public const string AdminPolicy = "AdminPolicy";
public const string ApplicantPolicy = "ApplicantPolicy";
public const string ApprovingAuthorityPolicy = "ApprovingAuthorityPolicy";
}
#pragma warning enable CS1591
}

View File

@@ -0,0 +1,22 @@
using ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.AdminRequests;
using ApplicationLayer.DataAccessingServices.Locations.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SchengenVisaApi.Common;
namespace SchengenVisaApi.Controllers
{
[ApiController]
[Route("admin")]
[Authorize(policy: PolicyConstants.AdminPolicy)]
public class AdminController(IEditLocationsRequestsHandler requestsHandler) : ControllerBase
{
[HttpPost]
[Route("country")]
public async Task<IActionResult> AddCountry(AddCountryRequest request, CancellationToken cancellationToken)
{
await requestsHandler.AddCountryAsync(request, cancellationToken);
return Ok();
}
}
}

View File

@@ -0,0 +1,16 @@
using ApplicationLayer.DataAccessingServices.Locations.RequestHandlers.ApplicantRequests;
using Microsoft.AspNetCore.Mvc;
namespace SchengenVisaApi.Controllers
{
[ApiController]
[Route("countries")]
public class LocationsController(ILocationRequestsHandler requestsHandler) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAvailableLocations(CancellationToken cancellationToken)
{
return Ok(await requestsHandler.HandleGetRequestAsync(cancellationToken));
}
}
}

View File

@@ -1,14 +1,13 @@
using ApplicationLayer.AuthServices.LoginService;
using ApplicationLayer.AuthServices.RegisterService;
using ApplicationLayer.AuthServices.Requests;
using Microsoft.AspNetCore.Identity.Data;
using ApplicationLayer.DataAccessingServices.AuthServices.LoginService;
using ApplicationLayer.DataAccessingServices.AuthServices.RegisterService;
using ApplicationLayer.DataAccessingServices.AuthServices.Requests;
using Microsoft.AspNetCore.Mvc;
namespace SchengenVisaApi.Controllers
{
[ApiController]
[Route("auth")]
public class UsersController(IRegisterService registerService, ILoginService loginService) : Controller
public class UsersController(IRegisterService registerService, ILoginService loginService) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Register(RegisterApplicantRequest request, CancellationToken cancellationToken)

View File

@@ -1,23 +1,29 @@
using System.Security.Claims;
using ApplicationLayer.DataAccessingServices.VisaApplications.Handlers;
using ApplicationLayer.DataAccessingServices.VisaApplications.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SchengenVisaApi.Common;
namespace SchengenVisaApi.Controllers;
[ApiController]
[Route("[controller]")]
public class VisaApplicationController(IVisaApplicationsRequestHandler visaApplicationsRequestHandler) : ControllerBase
[Authorize(policy: PolicyConstants.ApplicantPolicy)]
public class VisaApplicationController(IVisaApplicationRequestsHandler visaApplicationRequestsHandler) : ControllerBase
{
//TODO remove
[HttpGet]
public async Task<IActionResult> Get(CancellationToken cancellationToken)
{
var result = await visaApplicationsRequestHandler.Get(cancellationToken);
var result = await visaApplicationRequestsHandler.Get(cancellationToken);
return Ok(result);
}
[HttpPost]
public void Create(VisaApplicationCreateRequest request, CancellationToken cancellationToken)
{
visaApplicationsRequestHandler.HandleCreateRequest(request, cancellationToken);
var userId = Guid.Parse(HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value);
visaApplicationRequestsHandler.HandleCreateRequest(userId, request, cancellationToken);
}
}

View File

@@ -1,10 +1,16 @@
using System.Reflection;
using System.Security.Claims;
using System.Text;
using ApplicationLayer;
using Domains.Users;
using Infrastructure;
using Infrastructure.Auth;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using SchengenVisaApi.Common;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace SchengenVisaApi;
@@ -19,7 +25,7 @@ public static class DependencyInjection
builder.Services
.AddInfrastructure(config, environment.IsDevelopment())
.AddApplicationLayer()
.AddApplicationLayer(environment.IsDevelopment())
.AddAuth(config)
.AddPresentation(environment);
}
@@ -52,7 +58,7 @@ public static class DependencyInjection
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts => opts.TokenValidationParameters = parameters);
services.AddAuthorization();
services.AddAuthorizationBuilder().ConfigureAuthorizationPolicies();
services.AddTokenGenerator(new TokenGeneratorOptions(
Issuer: parameters.ValidIssuer!,
@@ -64,9 +70,24 @@ public static class DependencyInjection
return services;
}
/// Configure roles
private static void ConfigureAuthorizationPolicies(this AuthorizationBuilder builder)
{
builder.AddPolicy(
PolicyConstants.AdminPolicy,
p => p.RequireClaim(ClaimTypes.Role, Role.Admin.ToString()))
.AddPolicy(
PolicyConstants.ApprovingAuthorityPolicy,
p => p.RequireClaim(ClaimTypes.Role, Role.ApprovingAuthority.ToString()))
.AddPolicy(
PolicyConstants.ApplicantPolicy,
p => p.RequireClaim(ClaimTypes.Role, Role.Applicant.ToString()));
}
/// Add swagger
private static void AddSwagger(this IServiceCollection services)
{
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen(options =>
{
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";