Added authentication and authorization, updated dependency injections, removed hard-coded connection string

This commit is contained in:
2024-08-17 16:28:35 +03:00
parent 026a104131
commit 7cbe3d9698
44 changed files with 419 additions and 49 deletions

View File

@@ -14,4 +14,8 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0-preview.7.24405.7" />
</ItemGroup>
<ItemGroup>
<Folder Include="DataAccessingServices\" />
</ItemGroup>
</Project>

View File

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

View File

@@ -0,0 +1,12 @@
using ApplicationLayer.AuthServices.Requests;
namespace ApplicationLayer.AuthServices.LoginService
{
/// Handles <see cref="UserLoginRequest"/>
public interface ILoginService
{
/// Handle <see cref="UserLoginRequest"/>
/// <returns>JWT-token</returns>
Task<string> LoginAsync(UserLoginRequest request, CancellationToken cancellationToken);
}
}

View File

@@ -0,0 +1,21 @@
using ApplicationLayer.AuthServices.LoginService.Exceptions;
using ApplicationLayer.AuthServices.NeededServices;
using ApplicationLayer.AuthServices.Requests;
namespace ApplicationLayer.AuthServices.LoginService
{
/// <inheritdoc cref="ILoginService"/>
public class LoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService
{
async Task<string> ILoginService.LoginAsync(UserLoginRequest request, CancellationToken cancellationToken)
{
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,9 @@
using Domains.Users;
namespace ApplicationLayer.AuthServices.NeededServices
{
public interface ITokenGenerator
{
string CreateToken(User user);
}
}

View File

@@ -0,0 +1,15 @@
using ApplicationLayer.GeneralNeededServices;
using Domains.Users;
namespace ApplicationLayer.AuthServices.NeededServices
{
/// Repository pattern for <see cref="User"/>
public interface IUsersRepository : IGenericRepository<User>
{
/// Find <see cref="User"/> by email
/// <param name="email"><see cref="User"/>'s email</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>User or null if not found</returns>
Task<User?> FindByEmailAsync(string email, CancellationToken cancellationToken);
}
}

View File

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

View File

@@ -0,0 +1,11 @@
using ApplicationLayer.AuthServices.Requests;
namespace ApplicationLayer.AuthServices.RegisterService
{
/// Handles <see cref="RegisterApplicantRequest"/>
public interface IRegisterService
{
/// Handle <see cref="RegisterApplicantRequest"/>
Task Register(RegisterApplicantRequest request, CancellationToken cancellationToken);
}
}

View File

@@ -0,0 +1,31 @@
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

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

View File

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

View File

@@ -1,7 +1,7 @@
using ApplicationLayer.GeneralNeededServices;
using Domains.ApplicantDomain;
namespace ApplicationLayer.Applicants.NeededServices;
namespace ApplicationLayer.DataAccessingServices.Applicants.NeededServices;
/// Repository pattern for <see cref="Applicant"/>
public interface IApplicantsRepository : IGenericRepository<Applicant> { }
public interface IApplicantsRepository : IGenericRepository<Applicant>;

View File

@@ -0,0 +1,6 @@
using ApplicationLayer.GeneralNeededServices;
using Domains.LocationDomain;
namespace ApplicationLayer.DataAccessingServices.Locations.NeededServices;
public interface ICitiesRepository : IGenericRepository<City>;

View File

@@ -1,6 +1,6 @@
using ApplicationLayer.GeneralNeededServices;
using Domains.LocationDomain;
namespace ApplicationLayer.Locations.NeededServices;
namespace ApplicationLayer.DataAccessingServices.Locations.NeededServices;
public interface ICountriesRepository : IGenericRepository<Country> { }
public interface ICountriesRepository : IGenericRepository<Country>;

View File

@@ -1,7 +1,7 @@
using ApplicationLayer.VisaApplications.Requests;
using ApplicationLayer.DataAccessingServices.VisaApplications.Requests;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.VisaApplications.Handlers;
namespace ApplicationLayer.DataAccessingServices.VisaApplications.Handlers;
public interface IVisaApplicationsRequestHandler
{

View File

@@ -1,11 +1,11 @@
using ApplicationLayer.Locations.NeededServices;
using ApplicationLayer.VisaApplications.Models;
using ApplicationLayer.VisaApplications.NeededServices;
using ApplicationLayer.VisaApplications.Requests;
using ApplicationLayer.DataAccessingServices.Locations.NeededServices;
using ApplicationLayer.DataAccessingServices.VisaApplications.Models;
using ApplicationLayer.DataAccessingServices.VisaApplications.NeededServices;
using ApplicationLayer.DataAccessingServices.VisaApplications.Requests;
using Domains.ApplicantDomain;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.VisaApplications.Handlers;
namespace ApplicationLayer.DataAccessingServices.VisaApplications.Handlers;
/// Handles visa requests
public class VisaApplicationRequestsHandler(
@@ -64,6 +64,7 @@ public class VisaApplicationRequestsHandler(
};
await applications.AddAsync(visaApplication, cancellationToken);
await applications.SaveAsync(cancellationToken);
}
private async Task<PastVisit> ConvertPastVisitModelToPastVisit(PastVisitModel model, CancellationToken cancellationToken)

View File

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

View File

@@ -1,4 +1,4 @@
namespace ApplicationLayer.VisaApplications.Models
namespace ApplicationLayer.DataAccessingServices.VisaApplications.Models
{
public class PastVisitModel
{

View File

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

View File

@@ -1,6 +1,6 @@
using ApplicationLayer.GeneralNeededServices;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.VisaApplications.NeededServices;
namespace ApplicationLayer.DataAccessingServices.VisaApplications.NeededServices;
public interface IVisaApplicationsRepository : IGenericRepository<VisaApplication> { }
public interface IVisaApplicationsRepository : IGenericRepository<VisaApplication>;

View File

@@ -1,8 +1,8 @@
using ApplicationLayer.VisaApplications.Models;
using ApplicationLayer.DataAccessingServices.VisaApplications.Models;
using Domains.ApplicantDomain;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.VisaApplications.Requests;
namespace ApplicationLayer.DataAccessingServices.VisaApplications.Requests;
/// Model of visa request from user
public record VisaApplicationCreateRequest(

View File

@@ -1,4 +1,6 @@
using ApplicationLayer.VisaApplications.Handlers;
using ApplicationLayer.AuthServices.LoginService;
using ApplicationLayer.AuthServices.RegisterService;
using ApplicationLayer.DataAccessingServices.VisaApplications.Handlers;
using Microsoft.Extensions.DependencyInjection;
namespace ApplicationLayer;
@@ -11,6 +13,9 @@ public static class DependencyInjection
{
services.AddScoped<IVisaApplicationsRequestHandler, VisaApplicationRequestsHandler>();
services.AddScoped<IRegisterService, RegisterService>();
services.AddScoped<ILoginService, LoginService>();
return services;
}
}

View File

@@ -0,0 +1,8 @@
namespace ApplicationLayer.GeneralNeededServices
{
public interface IDateTimeProvider
{
/// Returns current date and time
DateTime Now();
}
}

View File

@@ -1,6 +0,0 @@
using ApplicationLayer.GeneralNeededServices;
using Domains.LocationDomain;
namespace ApplicationLayer.Locations.NeededServices;
public interface ICitiesRepository : IGenericRepository<City> { }

View File

@@ -0,0 +1,13 @@
namespace Domains.Users
{
/// Role of <see cref="User"/>
public enum Role
{
/// Requests visa applications
Applicant,
/// Approves or declines applications
ApprovingAuthority,
/// Manages approving authorities
Admin
}
}

View File

@@ -0,0 +1,14 @@
namespace Domains.Users
{
public class User : IEntity
{
/// Unique Identifier of <see cref="User"/>
public Guid Id { get; private set; } = Guid.NewGuid();
public Role Role { get; set; }
public string Email { get; set; } = null!;
public string Password { get; set; } = null!;
}
}

View File

@@ -0,0 +1,24 @@
using System.IdentityModel.Tokens.Jwt;
using ApplicationLayer.AuthServices.NeededServices;
using ApplicationLayer.GeneralNeededServices;
using Microsoft.Extensions.DependencyInjection;
namespace Infrastructure.Auth
{
public static class ServiceCollectionsExtensions
{
public static IServiceCollection AddTokenGenerator(this IServiceCollection services, TokenGeneratorOptions options)
{
services.AddSingleton<JwtSecurityTokenHandler>();
services.AddSingleton<ITokenGenerator, TokenGenerator>(provider =>
{
var tokenHandler = provider.GetRequiredService<JwtSecurityTokenHandler>();
var dateTimeProvider = provider.GetRequiredService<IDateTimeProvider>();
return new TokenGenerator(options, tokenHandler, dateTimeProvider);
});
return services;
}
}
}

View File

@@ -0,0 +1,30 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using ApplicationLayer.AuthServices.NeededServices;
using ApplicationLayer.GeneralNeededServices;
using Domains.Users;
namespace Infrastructure.Auth
{
public class TokenGenerator(TokenGeneratorOptions options, JwtSecurityTokenHandler tokenHandler, IDateTimeProvider dateTimeProvider)
: ITokenGenerator
{
public string CreateToken(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.Role, user.Role.ToString()),
new(ClaimTypes.Email, user.Email)
};
var token = new JwtSecurityToken(
issuer: options.Issuer,
audience: options.Audience,
expires: dateTimeProvider.Now().Add(options.ValidTime),
signingCredentials: options.Credentials,
claims: claims);
return tokenHandler.WriteToken(token);
}
}
}

View File

@@ -0,0 +1,6 @@
using Microsoft.IdentityModel.Tokens;
namespace Infrastructure.Auth
{
public record TokenGeneratorOptions(string Issuer, string Audience, TimeSpan ValidTime, SigningCredentials Credentials);
}

View File

@@ -0,0 +1,10 @@
using ApplicationLayer.GeneralNeededServices;
namespace Infrastructure.Common
{
/// Implements <see cref="IDateTimeProvider"/>
public class DateTimeProvider : IDateTimeProvider
{
DateTime IDateTimeProvider.Now() => DateTime.Now;
}
}

View File

@@ -1,4 +1,4 @@
using ApplicationLayer.Applicants.NeededServices;
using ApplicationLayer.DataAccessingServices.Applicants.NeededServices;
using Domains.ApplicantDomain;
using Infrastructure.Database.Generic;
using Microsoft.EntityFrameworkCore;

View File

@@ -1,4 +1,4 @@
using ApplicationLayer.Locations.NeededServices;
using ApplicationLayer.DataAccessingServices.Locations.NeededServices;
using Domains.LocationDomain;
using Infrastructure.Database.Generic;
using Microsoft.EntityFrameworkCore;

View File

@@ -1,4 +1,4 @@
using ApplicationLayer.Locations.NeededServices;
using ApplicationLayer.DataAccessingServices.Locations.NeededServices;
using Domains.LocationDomain;
using Infrastructure.Database.Generic;
using Microsoft.EntityFrameworkCore;

View File

@@ -0,0 +1,22 @@
using Domains.Users;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Infrastructure.Database.Users.Configuration
{
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> entity)
{
entity.Property(u => u.Email)
.IsUnicode(false)
.HasMaxLength(254);
entity.HasIndex(u => u.Email).IsUnique();
entity.Property(u => u.Password)
.IsUnicode(false)
.HasMaxLength(50);
}
}
}

View File

@@ -0,0 +1,17 @@
using ApplicationLayer.AuthServices.NeededServices;
using Domains.Users;
using Infrastructure.Database.Generic;
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
{
async Task<User?> IUsersRepository.FindByEmailAsync(string email, CancellationToken cancellationToken)
{
return await LoadDomain().SingleOrDefaultAsync(u => u.Email == email, cancellationToken);
}
}
}

View File

@@ -1,4 +1,4 @@
using ApplicationLayer.VisaApplications.NeededServices;
using ApplicationLayer.DataAccessingServices.VisaApplications.NeededServices;
using Domains.VisaApplicationDomain;
using Infrastructure.Database.Generic;
using Microsoft.EntityFrameworkCore;

View File

@@ -1,13 +1,19 @@
using ApplicationLayer.Applicants.NeededServices;
using ApplicationLayer.Locations.NeededServices;
using ApplicationLayer.VisaApplications.NeededServices;
using ApplicationLayer.AuthServices.NeededServices;
using ApplicationLayer.DataAccessingServices.Applicants.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;
using Infrastructure.Database.Locations.Repositories.Countries;
using Infrastructure.Database.Users.Repositories;
using Infrastructure.Database.VisaApplications.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using DbContext = Infrastructure.Database.DbContext;
@@ -17,11 +23,14 @@ namespace Infrastructure;
public static class DependencyInjection
{
/// Add services needed for Infrastructure layer
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
public static IServiceCollection AddInfrastructure(this IServiceCollection services,
IConfigurationManager configurationManager,
bool isDevelopment)
{
//TODO строка подключения
var databaseName = isDevelopment ? "developmentDB" : "normal'naya database";
services.AddDbContextFactory<DbContext>(opts =>
opts.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=visadb;Integrated Security=True;"));
opts.UseSqlServer(configurationManager.GetConnectionString(databaseName)));
services.AddScoped<IGenericReader>(serviceProvider => serviceProvider.GetRequiredService<DbContext>());
services.AddScoped<IGenericWriter>(serviceProvider => serviceProvider.GetRequiredService<DbContext>());
@@ -31,6 +40,9 @@ public static class DependencyInjection
services.AddScoped<IVisaApplicationsRepository, VisaApplicationsRepository>();
services.AddScoped<ICitiesRepository, CitiesRepository>();
services.AddScoped<ICountriesRepository, CountriesRepository>();
services.AddScoped<IUsersRepository, UsersRepository>();
services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
return services;
}

View File

@@ -11,6 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0-preview.7.24405.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0-preview.7.24405.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0-preview.7.24405.3" />

View File

@@ -0,0 +1,27 @@
using ApplicationLayer.AuthServices.LoginService;
using ApplicationLayer.AuthServices.RegisterService;
using ApplicationLayer.AuthServices.Requests;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.AspNetCore.Mvc;
namespace SchengenVisaApi.Controllers
{
[ApiController]
[Route("auth")]
public class UsersController(IRegisterService registerService, ILoginService loginService) : Controller
{
[HttpPost]
public async Task<IActionResult> Register(RegisterApplicantRequest request, CancellationToken cancellationToken)
{
await registerService.Register(request, cancellationToken);
return Created();
}
[HttpGet]
public async Task<IActionResult> Login(string email, string password, CancellationToken cancellationToken)
{
var result = await loginService.LoginAsync(new UserLoginRequest(email, password), cancellationToken);
return Ok(result);
}
}
}

View File

@@ -1,5 +1,5 @@
using ApplicationLayer.VisaApplications.Handlers;
using ApplicationLayer.VisaApplications.Requests;
using ApplicationLayer.DataAccessingServices.VisaApplications.Handlers;
using ApplicationLayer.DataAccessingServices.VisaApplications.Requests;
using Microsoft.AspNetCore.Mvc;
namespace SchengenVisaApi.Controllers;

View File

@@ -1,6 +1,10 @@
using System.Reflection;
using System.Text;
using ApplicationLayer;
using Infrastructure;
using Infrastructure.Auth;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
namespace SchengenVisaApi;
@@ -8,21 +12,61 @@ namespace SchengenVisaApi;
public static class DependencyInjection
{
/// Add needed services
public static IServiceCollection RegisterServices(this IServiceCollection services)
public static void RegisterServices(this WebApplicationBuilder builder)
{
services
.AddInfrastructure()
var config = builder.Configuration;
var environment = builder.Environment;
builder.Services
.AddInfrastructure(config, environment.IsDevelopment())
.AddApplicationLayer()
.AddPresentation();
.AddAuth(config)
.AddPresentation(environment);
}
/// Add services needed for Presentation layer
private static void AddPresentation(this IServiceCollection services,
IWebHostEnvironment environment)
{
if (environment.IsDevelopment())
{
services.AddSwagger();
}
services.AddControllers();
}
/// Adds authentication, authorization and token generator
private static IServiceCollection AddAuth(this IServiceCollection services, IConfigurationManager configurationManager)
{
var parameters = new TokenValidationParameters
{
ValidIssuer = configurationManager["JwtSettings:Issuer"],
ValidAudience = configurationManager["JwtSettings:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configurationManager["JwtSettings:Key"]!)),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts => opts.TokenValidationParameters = parameters);
services.AddAuthorization();
services.AddTokenGenerator(new TokenGeneratorOptions(
Issuer: parameters.ValidIssuer!,
Audience: parameters.ValidAudience!,
Credentials: new SigningCredentials(parameters.IssuerSigningKey, SecurityAlgorithms.HmacSha256),
ValidTime: TimeSpan.FromMinutes(30)
));
return services;
}
/// Add services needed for Presentation layer
private static void AddPresentation(this IServiceCollection services)
/// Add swagger
private static void AddSwagger(this IServiceCollection services)
{
services.AddControllers();
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(options =>
{
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";

View File

@@ -1,11 +1,12 @@
namespace SchengenVisaApi;
#pragma warning disable CS1591
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.RegisterServices();
builder.RegisterServices();
var app = builder.Build();
app.ConfigurePipelineRequest();
@@ -13,4 +14,4 @@ public class Program
app.Run();
}
}
#pragma warning restore CS1591
#pragma warning restore CS1591

View File

@@ -11,8 +11,11 @@ public static class PipelineRequest
app.UseHttpsRedirection();
app.UseAuthentication()
.UseAuthorization();
app.MapControllers();
return app;
}
}
}

View File

@@ -5,5 +5,16 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"ConnectionStrings": {
"developmentDB": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=visadb;Integrated Security=True;",
"normal'naya db": ""
},
"JwtSettings": {
"Issuer":"visaAPI",
"Audience":"visaClient",
"Key": "frsjiajfapojrpwauflakpiowaidoaplakrf"
}
}