Merge pull request 'Backend login and register' (#5) from feature/dotnet into develop
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
103
.idea/.idea.DotNetAngular/.idea/workspace.xml
generated
103
.idea/.idea.DotNetAngular/.idea/workspace.xml
generated
@@ -13,17 +13,68 @@
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="1ac72a4a-52ad-4e70-9b15-c330b1ed3e7a" name="Changes" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/inspectionProfiles/Project_Default.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/API/Controllers/AuthController.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/API/Controllers/UserController.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/API/Extension/ResultExtension.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Common/Results/Error.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Common/Results/ErrorTypeConstant.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Common/Results/Result.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Common/Results/TResult.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/DTOs/PagedResult.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/DTOs/ResetPasswordDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/DTOs/UserDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Errors/AuthError.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Errors/UserError.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Extensions/ServiceCollectionExtensions.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Interfaces/IAuthenticationService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Interfaces/IEmailService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Interfaces/IJwtService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Interfaces/IUserService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Models/AssingRoleRequest.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Models/EmailRequest.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Models/LoginRequest.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Models/RefreshTokenRequest.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Models/RegisterRequest.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Models/TokenResponse.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Models/UserUpdateRequest.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Services/AuthenticationService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Services/EmailService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Services/JwtService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Services/UserService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Validators/LoginRequestValidator.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Validators/RegisterRequestValidator.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Application/Validators/UserUpdateRequestValidator.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/package-lock.json" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Domain/Entities/Role.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Domain/Entities/User.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Domain/Entities/UserRole.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Domain/Interface/IGenericRepository.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Domain/Interface/IUnitOfWork.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Domain/Interface/IUserRepository.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Domain/Interface/IUserRoleRepository.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Configuration/RoleConfiguration.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Configuration/UserConfiguration.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Configuration/UserRoleConfiguration.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Context/ApplicationDbContext.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Context/ApplicationDbContextFactory.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Migrations/20260206104345_InitialCreate.Designer.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Migrations/20260206104345_InitialCreate.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Repositories/GenericRepository.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Repositories/UnitOfWork.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Repositories/UserRepository.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Repositories/UserRoleRepository.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Utilities/EmailBody.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Utilities/GenerateRefreshTokenHelper.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Infrastructure/Utilities/PostgreSqlSettings.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/API/API.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/API/API.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/API/Extension/ServiceCollectionExtensions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/API/Extension/ServiceCollectionExtensions.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/API/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/API/Program.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/API/appsettings.json" beforeDir="false" afterPath="$PROJECT_DIR$/src/API/appsettings.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Application/Application.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/Application/Application.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/user.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/user.service.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Domain/Domain.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/Domain/Domain.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Infrastructure/Infrastructure.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/Infrastructure/Infrastructure.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/tests/Application.FunctionalTest/Application.FunctionalTest.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/tests/Application.FunctionalTest/Application.FunctionalTest.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/tests/Application.UnitTest/Application.UnitTest.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/tests/Application.UnitTest/Application.UnitTest.csproj" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -34,7 +85,7 @@
|
||||
<option name="firstShow" value="false" />
|
||||
</component>
|
||||
<component name="EmbeddingIndexingInfo">
|
||||
<option name="cachedIndexableFilesCount" value="2" />
|
||||
<option name="cachedIndexableFilesCount" value="17" />
|
||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
@@ -85,7 +136,7 @@
|
||||
}
|
||||
}</component>
|
||||
<component name="RecapUselessUpdatesCounter">
|
||||
<option name="suspendCountdown" value="1" />
|
||||
<option name="suspendCountdown" value="0" />
|
||||
</component>
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.API: Angular_dev">
|
||||
<configuration name="Application.FunctionalTest" type="DotNetProject" factoryName=".NET Project">
|
||||
@@ -210,7 +261,15 @@
|
||||
<workItem from="1770204235235" duration="1487000" />
|
||||
<workItem from="1770294298887" duration="1216000" />
|
||||
<workItem from="1770295524085" duration="8729000" />
|
||||
<workItem from="1770308954185" duration="2271000" />
|
||||
<workItem from="1770308954185" duration="9016000" />
|
||||
<workItem from="1770369615769" duration="3456000" />
|
||||
<workItem from="1770373101221" duration="2750000" />
|
||||
<workItem from="1770377559983" duration="818000" />
|
||||
<workItem from="1770379380312" duration="1252000" />
|
||||
<workItem from="1770382791102" duration="1343000" />
|
||||
<workItem from="1771239672703" duration="359000" />
|
||||
<workItem from="1771331713319" duration="2451000" />
|
||||
<workItem from="1771503996420" duration="1283000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="updating template">
|
||||
<option name="closed" value="true" />
|
||||
@@ -236,7 +295,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770306489835</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="4" />
|
||||
<task id="LOCAL-00004" summary="update to dotnet 10">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770318472632</created>
|
||||
<option name="number" value="00004" />
|
||||
<option name="presentableId" value="LOCAL-00004" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770318472632</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="5" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -245,6 +312,21 @@
|
||||
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="RECENT_FILTERS">
|
||||
<map>
|
||||
<entry key="Branch">
|
||||
<value>
|
||||
<list>
|
||||
<RecentGroup>
|
||||
<option name="FILTER_VALUES">
|
||||
<option value="feature/dotnet" />
|
||||
</option>
|
||||
</RecentGroup>
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
@@ -260,7 +342,8 @@
|
||||
<MESSAGE value="updating template" />
|
||||
<MESSAGE value="update to angular 20" />
|
||||
<MESSAGE value="login.component, register.component" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="login.component, register.component" />
|
||||
<MESSAGE value="update to dotnet 10" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="update to dotnet 10" />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>9c947c10-2373-4590-92a9-e5fe6b759c69</UserSecretsId>
|
||||
<SpaRoot>..\ClientApp\</SpaRoot>
|
||||
<SpaProxyServerUrl>http://localhost:44492</SpaProxyServerUrl>
|
||||
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
|
||||
@@ -16,4 +17,8 @@
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application\Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
45
src/API/Controllers/AuthController.cs
Normal file
45
src/API/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using API.Extension;
|
||||
using Application.DTOs;
|
||||
using Application.Interfaces;
|
||||
using Application.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class AuthController(IAuthenticationService authService) : BaseApiController
|
||||
{
|
||||
[HttpPost("register")]
|
||||
public async Task<IResult> Register(RegisterRequest registerRequest)
|
||||
{
|
||||
var response = await authService.RegisterAsync(registerRequest);
|
||||
return response.ToHttpResponse();
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IResult> Login(LoginRequest loginRequest)
|
||||
{
|
||||
var response = await authService.LoginAsync(loginRequest);
|
||||
return response.ToHttpResponse();
|
||||
}
|
||||
|
||||
[HttpPost("refresh-token")]
|
||||
public async Task<IResult> RefreshToken(RefreshTokenRequest refreshTokenRequest)
|
||||
{
|
||||
var response = await authService.RefreshTokensAsync(refreshTokenRequest);
|
||||
return response.ToHttpResponse();
|
||||
}
|
||||
|
||||
[HttpPost("send-reset-email/{email}")]
|
||||
public async Task<IResult> SendResetEmail(string email)
|
||||
{
|
||||
var response = await authService.SendResetEmailAsync(email);
|
||||
return response.ToHttpResponse();
|
||||
}
|
||||
|
||||
[HttpPost("reset-password")]
|
||||
public async Task<IResult> ResetPassword(ResetPasswordDto resetPasswordDto)
|
||||
{
|
||||
var response = await authService.ResetPasswordAsync(resetPasswordDto);
|
||||
return response.ToHttpResponse();
|
||||
}
|
||||
}
|
||||
63
src/API/Controllers/UserController.cs
Normal file
63
src/API/Controllers/UserController.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using API.Extension;
|
||||
using Application.Interfaces;
|
||||
using Application.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class UserController(IUserService userService) : BaseApiController
|
||||
{
|
||||
[Authorize(Roles = "SuperAdmin, Admin")]
|
||||
[HttpGet]
|
||||
public async Task<IResult> GetAllUsers(
|
||||
[FromQuery] int pageNumber = 1,
|
||||
[FromQuery] int pageSize = 10)
|
||||
{
|
||||
var users = await userService.GetAsync(pageNumber, pageSize);
|
||||
return users.ToHttpResponse();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPut]
|
||||
public async Task<IResult> UpdateUser([FromBody] UserUpdateRequest userUpdateRequest)
|
||||
{
|
||||
var result = await userService.UpdateAsync(userUpdateRequest);
|
||||
return result.ToHttpResponse();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IResult> DeleteUser(int id)
|
||||
{
|
||||
var currentUserId = int.Parse(User.FindFirst("UserId")!.Value);
|
||||
var result = await userService.DeleteAsync(id, currentUserId);
|
||||
return result.ToHttpResponse();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IResult> GetUserById(int id)
|
||||
{
|
||||
var user = await userService.GetUserByIdAsync(id);
|
||||
return user.ToHttpResponse();
|
||||
}
|
||||
|
||||
[Authorize(Roles = "SuperAdmin")]
|
||||
[HttpPost("assign-role")]
|
||||
public async Task<IResult> AssignRole([FromBody] AssingRoleRequest roleRequest)
|
||||
|
||||
{
|
||||
var result = await userService.AssignRoleAsync(roleRequest);
|
||||
return result.ToHttpResponse();
|
||||
}
|
||||
|
||||
[Authorize(Roles = "SuperAdmin")]
|
||||
[HttpDelete("revoke-role")]
|
||||
public async Task<IResult> RevokeRole([FromBody] AssingRoleRequest roleRequest)
|
||||
{
|
||||
var result = await userService.RevokeRoleAsync(roleRequest);
|
||||
return result.ToHttpResponse();
|
||||
}
|
||||
|
||||
}
|
||||
32
src/API/Extension/ResultExtension.cs
Normal file
32
src/API/Extension/ResultExtension.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Application.Common.Results;
|
||||
|
||||
namespace API.Extension;
|
||||
|
||||
public static class ResultExtension
|
||||
{
|
||||
public static IResult ToHttpResponse(this Result result)
|
||||
{
|
||||
if (result.IsSuccess) return Results.Ok(result);
|
||||
|
||||
return MapErrorResponse(result.Error, result);
|
||||
}
|
||||
|
||||
public static IResult ToHttpResponse<T>(this Result<T> result)
|
||||
{
|
||||
if (result.IsSuccess)
|
||||
return Results.Ok(result);
|
||||
return MapErrorResponse(result.Error, result);
|
||||
}
|
||||
|
||||
private static IResult MapErrorResponse(Error? error, object result)
|
||||
{
|
||||
return error?.Code switch
|
||||
{
|
||||
ErrorTypeConstant.ValidationError => Results.BadRequest(result),
|
||||
ErrorTypeConstant.NotFound => Results.NotFound(result),
|
||||
ErrorTypeConstant.Forbidden => Results.Forbid(),
|
||||
ErrorTypeConstant.Unauthorized => Results.Unauthorized(),
|
||||
_ => Results.Problem(error?.Message, statusCode: 500)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ namespace API.Extension;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddWebServices(this IServiceCollection services)
|
||||
public static IServiceCollection AddWebServices(this IServiceCollection services,
|
||||
ConfigurationManager builderConfiguration)
|
||||
{
|
||||
services.AddSwaggerGen(options =>
|
||||
{
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
using API.Extension;
|
||||
using Application.Extensions;
|
||||
using Infrastructure.Context;
|
||||
using Infrastructure.Extensions;
|
||||
using Infrastructure.Utilities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
@@ -7,7 +12,18 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
// Add services to the container.
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddWebServices();
|
||||
builder.Services.AddWebServices(builder.Configuration);
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
builder.Services.AddApplication();
|
||||
|
||||
// PostgreSql Database for development
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
{
|
||||
var postgreSqlSettings =
|
||||
builder.Configuration.GetRequiredSection("PostgreSqlSettings").Get<PostgreSqlSettings>();
|
||||
var connectionString = postgreSqlSettings?.ConnectionString;
|
||||
options.UseNpgsql(connectionString);
|
||||
});
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
|
||||
@@ -5,5 +5,18 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"PostgreSqlSettings": {
|
||||
"Database": "template_db",
|
||||
"Username": "natlinux"
|
||||
},
|
||||
"jwt": {
|
||||
"Key": "veryveryveryveryveryveryverysecretkey",
|
||||
"Issuer": "https://localhost:7091",
|
||||
"Audience": "http://localhost:5184"
|
||||
},
|
||||
"EmailSettings": {
|
||||
"SmtpServer": "smtp.gmail.com",
|
||||
"Port": 465
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,20 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="MailKit" Version="4.14.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.3.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.3.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
|
||||
<PackageReference Include="MimeKit" Version="4.14.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
6
src/Application/Common/Results/Error.cs
Normal file
6
src/Application/Common/Results/Error.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Application.Common.Results;
|
||||
|
||||
public sealed record Error(string Code, string Message)
|
||||
{
|
||||
internal static Error None => new(ErrorTypeConstant.None, string.Empty);
|
||||
}
|
||||
11
src/Application/Common/Results/ErrorTypeConstant.cs
Normal file
11
src/Application/Common/Results/ErrorTypeConstant.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Application.Common.Results;
|
||||
|
||||
public class ErrorTypeConstant
|
||||
{
|
||||
public const string None = "None";
|
||||
public const string ValidationError = "ValidationError";
|
||||
public const string InternalServerError = "InternalServerError";
|
||||
public const string NotFound = "NotFoundError";
|
||||
public const string Unauthorized = "UnauthorizedError";
|
||||
public const string Forbidden = "ForbiddenError";
|
||||
}
|
||||
37
src/Application/Common/Results/Result.cs
Normal file
37
src/Application/Common/Results/Result.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace Application.Common.Results;
|
||||
|
||||
public class Result
|
||||
{
|
||||
protected Result(bool isSuccess, Error error)
|
||||
{
|
||||
if ((isSuccess && error != Error.None) || (!isSuccess && error == Error.None))
|
||||
throw new InvalidOperationException("Invalid operation");
|
||||
IsSuccess = isSuccess;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public bool IsSuccess { get; }
|
||||
public bool IsFailure => !IsSuccess;
|
||||
|
||||
public Error Error { get; }
|
||||
|
||||
public static Result Success()
|
||||
{
|
||||
return new Result(true, Error.None);
|
||||
}
|
||||
|
||||
public static Result<TValue> Success<TValue>(TValue value)
|
||||
{
|
||||
return new Result<TValue>(value, true, Error.None);
|
||||
}
|
||||
|
||||
public static Result Failure(Error error)
|
||||
{
|
||||
return new Result(false, error);
|
||||
}
|
||||
|
||||
public static Result<TValue> Failure<TValue>(Error error)
|
||||
{
|
||||
return new Result<TValue>(default!, false, error);
|
||||
}
|
||||
}
|
||||
13
src/Application/Common/Results/TResult.cs
Normal file
13
src/Application/Common/Results/TResult.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Application.Common.Results;
|
||||
|
||||
public class Result<TValue> : Result
|
||||
{
|
||||
private readonly TValue _value;
|
||||
|
||||
protected internal Result(TValue value, bool isSuccess, Error error) : base(isSuccess, error)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public TValue Value => IsSuccess ? _value : throw new InvalidOperationException("No value for failure result");
|
||||
}
|
||||
12
src/Application/DTOs/PagedResult.cs
Normal file
12
src/Application/DTOs/PagedResult.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Application.DTOs;
|
||||
|
||||
public class PagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = [];
|
||||
public int TotalCount { get; set; }
|
||||
public int PageNumber { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||
public bool HasPreviousPage => PageNumber > 1;
|
||||
public bool HasNext => PageNumber < TotalPages;
|
||||
}
|
||||
8
src/Application/DTOs/ResetPasswordDto.cs
Normal file
8
src/Application/DTOs/ResetPasswordDto.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Application.DTOs;
|
||||
|
||||
public record ResetPasswordDto
|
||||
{
|
||||
public required string Email { get; init; }
|
||||
public required string EmailToken { get; init; }
|
||||
public required string NewPassword { get; init; }
|
||||
}
|
||||
10
src/Application/DTOs/UserDto.cs
Normal file
10
src/Application/DTOs/UserDto.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Application.DTOs;
|
||||
|
||||
public record UserDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public required string Email { get; init; }
|
||||
public required string Username { get; init; }
|
||||
public DateTime LastLogin { get; init; }
|
||||
public List<string> Roles { get; init; } = [];
|
||||
}
|
||||
29
src/Application/Errors/AuthError.cs
Normal file
29
src/Application/Errors/AuthError.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Application.Common.Results;
|
||||
|
||||
namespace Application.Errors;
|
||||
|
||||
public static class AuthError
|
||||
{
|
||||
public static Error InvalidRegisterRequest => new(ErrorTypeConstant.ValidationError, "Invalid register request");
|
||||
|
||||
public static Error EmailAlreadyExists => new(ErrorTypeConstant.ValidationError, "E-Mail already exists");
|
||||
|
||||
public static Error UsernameAlreadyExists => new(ErrorTypeConstant.ValidationError, "Username already exists");
|
||||
public static Error InvalidLoginRequest => new(ErrorTypeConstant.ValidationError, "Invalid login request");
|
||||
|
||||
public static Error UserNotFound => new(ErrorTypeConstant.NotFound, "User not found");
|
||||
|
||||
public static Error InvalidPassword => new(ErrorTypeConstant.ValidationError, "Invalid Password");
|
||||
public static Error InvalidResetLink => new(ErrorTypeConstant.ValidationError, "Invalid reset link");
|
||||
|
||||
|
||||
public static Error CreateInvalidLoginRequestError(IEnumerable<string> errors)
|
||||
{
|
||||
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
|
||||
}
|
||||
|
||||
public static Error CreateInvalidRegisterRequestError(IEnumerable<string> errors)
|
||||
{
|
||||
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
|
||||
}
|
||||
}
|
||||
54
src/Application/Errors/UserError.cs
Normal file
54
src/Application/Errors/UserError.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Application.Common.Results;
|
||||
|
||||
namespace Application.Errors;
|
||||
|
||||
public static class UserError
|
||||
{
|
||||
public static Error InternalServerError =>
|
||||
new(ErrorTypeConstant.InternalServerError, "something went wrong");
|
||||
|
||||
public static Error UserNotFound =>
|
||||
new(ErrorTypeConstant.NotFound, "User not found");
|
||||
|
||||
public static Error UserCookieConsentNotFound =>
|
||||
new(ErrorTypeConstant.NotFound, "Cookie consent not found for user");
|
||||
|
||||
public static Error FailedToAssignRole =>
|
||||
new(ErrorTypeConstant.InternalServerError, "failed to assign role");
|
||||
|
||||
public static Error FailedToRevokeRole =>
|
||||
new(ErrorTypeConstant.InternalServerError, "failed to revoke role");
|
||||
|
||||
public static Error UserAlreadyHasRole =>
|
||||
new(ErrorTypeConstant.ValidationError, "User already has role");
|
||||
|
||||
public static Error UserHasNoRole =>
|
||||
new(ErrorTypeConstant.ValidationError, "User already has no role");
|
||||
|
||||
public static Error FailedToRevokeCookieConsent =>
|
||||
new(ErrorTypeConstant.InternalServerError, "failed to revoke cookie consent");
|
||||
|
||||
public static Error UserAlreadyHasCookieConsent =>
|
||||
new(ErrorTypeConstant.ValidationError, "User already has cookie consent");
|
||||
|
||||
public static Error UserHasNoCookieConsent =>
|
||||
new(ErrorTypeConstant.ValidationError, "User already has no cookie consent");
|
||||
|
||||
public static Error CannotDeleteYourself =>
|
||||
new(ErrorTypeConstant.ValidationError, "You cannot delete your own account");
|
||||
|
||||
public static Error CreateInvalidUserUpdateRequestError(IEnumerable<string> errors)
|
||||
{
|
||||
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
|
||||
}
|
||||
|
||||
public static Error CreateInvalidCookieConsentError(IEnumerable<string> errors)
|
||||
{
|
||||
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
|
||||
}
|
||||
|
||||
public static Error CreateInvalidLoginRequestError(IEnumerable<string> errors)
|
||||
{
|
||||
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
|
||||
}
|
||||
}
|
||||
20
src/Application/Extensions/ServiceCollectionExtensions.cs
Normal file
20
src/Application/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Application.Interfaces;
|
||||
using Application.Services;
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Application.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddApplication(this IServiceCollection services)
|
||||
{
|
||||
services.AddValidatorsFromAssembly(typeof(ServiceCollectionExtensions).Assembly);
|
||||
services.AddScoped<IAuthenticationService, AuthenticationService>();
|
||||
services.AddScoped<IJwtService, JwtService>();
|
||||
services.AddScoped<IEmailService, EmailService>();
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
14
src/Application/Interfaces/IAuthenticationService.cs
Normal file
14
src/Application/Interfaces/IAuthenticationService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Application.Common.Results;
|
||||
using Application.DTOs;
|
||||
using Application.Models;
|
||||
|
||||
namespace Application.Interfaces;
|
||||
|
||||
public interface IAuthenticationService
|
||||
{
|
||||
Task<Result> RegisterAsync(RegisterRequest request);
|
||||
Task<Result> LoginAsync(LoginRequest request);
|
||||
Task<Result> RefreshTokensAsync(RefreshTokenRequest request);
|
||||
Task<Result> SendResetEmailAsync(string email);
|
||||
Task<Result> ResetPasswordAsync(ResetPasswordDto resetPasswordDto);
|
||||
}
|
||||
8
src/Application/Interfaces/IEmailService.cs
Normal file
8
src/Application/Interfaces/IEmailService.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Application.Models;
|
||||
|
||||
namespace Application.Interfaces;
|
||||
|
||||
public interface IEmailService
|
||||
{
|
||||
void SendEmailAsync(EmailRequest emailRequest);
|
||||
}
|
||||
10
src/Application/Interfaces/IJwtService.cs
Normal file
10
src/Application/Interfaces/IJwtService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Domain.Entities;
|
||||
|
||||
namespace Application.Interfaces;
|
||||
|
||||
public interface IJwtService
|
||||
{
|
||||
Task<string> GenerateTokenAsync(User user);
|
||||
Task<string> GenerateAndSaveRefreshTokenAsync(User user);
|
||||
Task<User?> ValidateRefreshTokenAsync(int userId, string refreshToken);
|
||||
}
|
||||
15
src/Application/Interfaces/IUserService.cs
Normal file
15
src/Application/Interfaces/IUserService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Application.Common.Results;
|
||||
using Application.DTOs;
|
||||
using Application.Models;
|
||||
|
||||
namespace Application.Interfaces;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
Task<Result<PagedResult<UserDto>>> GetAsync(int pageNumber = 1, int pageSize = 10);
|
||||
Task<Result<string>> UpdateAsync(UserUpdateRequest user);
|
||||
Task<Result<string>> DeleteAsync(int id, int currentUserId);
|
||||
Task<Result<UserDto>> GetUserByIdAsync(int id);
|
||||
Task<Result<string>> AssignRoleAsync(AssingRoleRequest roleRequest);
|
||||
Task<Result<string>> RevokeRoleAsync(AssingRoleRequest roleRequest);
|
||||
}
|
||||
3
src/Application/Models/AssingRoleRequest.cs
Normal file
3
src/Application/Models/AssingRoleRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public record AssingRoleRequest(int UserId, int RoleId);
|
||||
15
src/Application/Models/EmailRequest.cs
Normal file
15
src/Application/Models/EmailRequest.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public class EmailRequest
|
||||
{
|
||||
public EmailRequest(string to, string subject, string content)
|
||||
{
|
||||
To = to;
|
||||
Subject = subject;
|
||||
Content = content;
|
||||
}
|
||||
|
||||
public string To { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public string Content { get; set; }
|
||||
}
|
||||
3
src/Application/Models/LoginRequest.cs
Normal file
3
src/Application/Models/LoginRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public record LoginRequest(string Email, string Password);
|
||||
7
src/Application/Models/RefreshTokenRequest.cs
Normal file
7
src/Application/Models/RefreshTokenRequest.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public class RefreshTokenRequest
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
public required string RefreshToken { get; set; }
|
||||
}
|
||||
7
src/Application/Models/RegisterRequest.cs
Normal file
7
src/Application/Models/RegisterRequest.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public record RegisterRequest(
|
||||
string Username,
|
||||
string Email,
|
||||
string Password
|
||||
);
|
||||
7
src/Application/Models/TokenResponse.cs
Normal file
7
src/Application/Models/TokenResponse.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public class TokenResponse
|
||||
{
|
||||
public required string AccessToken { get; set; }
|
||||
public required string RefreshToken { get; set; }
|
||||
}
|
||||
3
src/Application/Models/UserUpdateRequest.cs
Normal file
3
src/Application/Models/UserUpdateRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public record UserUpdateRequest(int Id, string Username, string Email);
|
||||
147
src/Application/Services/AuthenticationService.cs
Normal file
147
src/Application/Services/AuthenticationService.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System.Security.Cryptography;
|
||||
using Application.Common.Results;
|
||||
using Application.DTOs;
|
||||
using Application.Errors;
|
||||
using Application.Interfaces;
|
||||
using Application.Models;
|
||||
using Application.Validators;
|
||||
using Domain.Entities;
|
||||
using Domain.Interface;
|
||||
using Infrastructure.Utilities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Application.Services;
|
||||
|
||||
public class AuthenticationService(
|
||||
IUnitOfWork unitOfWork,
|
||||
IUserRepository iUserRepository,
|
||||
LoginRequestValidator loginRequestValidator,
|
||||
RegisterRequestValidator registerRequestValidator,
|
||||
IJwtService jwtService,
|
||||
IEmailService emailService) : IAuthenticationService
|
||||
{
|
||||
public async Task<Result> RegisterAsync(RegisterRequest registerRequest)
|
||||
{
|
||||
var validationResult = await registerRequestValidator.ValidateAsync(registerRequest);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
var errors = validationResult.Errors.Select(x => x.ErrorMessage);
|
||||
return Result.Failure(AuthError.CreateInvalidRegisterRequestError(errors));
|
||||
}
|
||||
|
||||
var emailExists = await iUserRepository.GetUserByEmailAsync(registerRequest.Email);
|
||||
if (emailExists is not null) return Result.Failure(AuthError.EmailAlreadyExists);
|
||||
|
||||
var usernameExists = await iUserRepository.GetUserByUsernameAsync(registerRequest.Username);
|
||||
if (usernameExists is not null) return Result.Failure(AuthError.UsernameAlreadyExists);
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Username = registerRequest.Username,
|
||||
Email = registerRequest.Email,
|
||||
Password = registerRequest.Password,
|
||||
UserRoles = [new UserRole { RoleId = 3 }]
|
||||
};
|
||||
|
||||
var passwordHasher = new PasswordHasher<User>();
|
||||
var hashedPassword = passwordHasher.HashPassword(user, registerRequest.Password);
|
||||
user.Password = hashedPassword;
|
||||
|
||||
await iUserRepository.AddAsync(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return Result.Success("User registered successfully.");
|
||||
}
|
||||
|
||||
public async Task<Result> LoginAsync(LoginRequest loginRequest)
|
||||
{
|
||||
var validationResult = await loginRequestValidator.ValidateAsync(loginRequest);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
var errors = validationResult.Errors.Select(x => x.ErrorMessage);
|
||||
return Result.Failure(AuthError.CreateInvalidLoginRequestError(errors));
|
||||
}
|
||||
|
||||
var (email, password) = loginRequest;
|
||||
var user = await iUserRepository.GetUserByEmailAsync(email);
|
||||
if (user is null) return Result.Failure(AuthError.UserNotFound);
|
||||
|
||||
var passwordHasher = new PasswordHasher<User>();
|
||||
var verificationResult = passwordHasher.VerifyHashedPassword(user, user.Password, password);
|
||||
if (verificationResult == PasswordVerificationResult.Failed)
|
||||
return Result.Failure(AuthError.InvalidPassword);
|
||||
|
||||
user.LastLogin = DateTime.UtcNow;
|
||||
iUserRepository.Update(user);
|
||||
|
||||
|
||||
var token = new TokenResponse
|
||||
{
|
||||
AccessToken = await jwtService.GenerateTokenAsync(user),
|
||||
RefreshToken = await jwtService.GenerateAndSaveRefreshTokenAsync(user)
|
||||
};
|
||||
var result = new
|
||||
{
|
||||
Token = token, user.Username
|
||||
};
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
|
||||
public async Task<Result> RefreshTokensAsync(RefreshTokenRequest request)
|
||||
{
|
||||
var user = await jwtService.ValidateRefreshTokenAsync(request.UserId, request.RefreshToken);
|
||||
if (user is null) return Result.Failure(AuthError.UserNotFound);
|
||||
|
||||
var result = new TokenResponse
|
||||
{
|
||||
AccessToken = await jwtService.GenerateTokenAsync(user),
|
||||
RefreshToken = await jwtService.GenerateAndSaveRefreshTokenAsync(user)
|
||||
};
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
|
||||
public async Task<Result> SendResetEmailAsync(string email)
|
||||
{
|
||||
var user = await iUserRepository.GetUserByEmailAsync(email);
|
||||
if (user is null) return Result.Failure(AuthError.UserNotFound);
|
||||
|
||||
var tokenBytes = RandomNumberGenerator.GetBytes(64);
|
||||
var emailToken = Convert.ToBase64String(tokenBytes);
|
||||
user.ResetPasswordToken = emailToken;
|
||||
user.ResetPasswordTokenExpiryTime = DateTime.UtcNow.AddMinutes(15);
|
||||
var emailModel = new EmailRequest(email, "Reset Password!!", EmailBody.EmailStringBody(email, emailToken));
|
||||
emailService.SendEmailAsync(emailModel);
|
||||
iUserRepository.Update(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return Result.Success("Reset email sent successfully");
|
||||
}
|
||||
|
||||
public async Task<Result> ResetPasswordAsync(ResetPasswordDto resetPasswordDto)
|
||||
{
|
||||
// Normalize the incoming token if '+' was converted to space by transport layers.
|
||||
var normalizedToken = (resetPasswordDto.EmailToken ?? string.Empty).Replace(" ", "+");
|
||||
|
||||
var user = await iUserRepository.GetUserByEmailAsync(resetPasswordDto.Email);
|
||||
if (user is null) return Result.Failure(AuthError.UserNotFound);
|
||||
|
||||
var tokenCode = user.ResetPasswordToken;
|
||||
var emailTokenExpiryTime = user.ResetPasswordTokenExpiryTime;
|
||||
|
||||
// Validate token and expiration using UTC to match stored times
|
||||
if (string.IsNullOrWhiteSpace(normalizedToken) || tokenCode != normalizedToken ||
|
||||
emailTokenExpiryTime < DateTime.UtcNow) return Result.Failure(AuthError.InvalidResetLink);
|
||||
|
||||
var passwordHasher = new PasswordHasher<User>();
|
||||
var hashedPassword = passwordHasher.HashPassword(user, resetPasswordDto.NewPassword);
|
||||
user.Password = hashedPassword;
|
||||
|
||||
// Invalidate the reset token after successful use
|
||||
user.ResetPasswordToken = null;
|
||||
user.ResetPasswordTokenExpiryTime = default;
|
||||
|
||||
iUserRepository.Update(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return Result.Success("Password reset successfully");
|
||||
}
|
||||
}
|
||||
49
src/Application/Services/EmailService.cs
Normal file
49
src/Application/Services/EmailService.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Application.Interfaces;
|
||||
using Application.Models;
|
||||
using MailKit.Net.Smtp;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MimeKit;
|
||||
using MimeKit.Text;
|
||||
|
||||
namespace Application.Services;
|
||||
|
||||
public class EmailService(IConfiguration config, Func<ISmtpClient>? smtpClientFactory)
|
||||
: IEmailService
|
||||
{
|
||||
private readonly Func<ISmtpClient> _smtpClientFactory = smtpClientFactory ?? (() => new SmtpClient());
|
||||
|
||||
public EmailService(IConfiguration config) : this(config, null)
|
||||
{
|
||||
}
|
||||
|
||||
public void SendEmailAsync(EmailRequest emailRequest)
|
||||
{
|
||||
var emailMessage = new MimeMessage();
|
||||
var from = config["EmailSettings:From"];
|
||||
emailMessage.From.Add(new MailboxAddress("RssReader", from));
|
||||
emailMessage.To.Add(new MailboxAddress(emailRequest.To, emailRequest.To));
|
||||
emailMessage.Subject = emailRequest.Subject;
|
||||
emailMessage.Body = new TextPart(TextFormat.Html)
|
||||
{
|
||||
Text = string.Format(emailRequest.Content)
|
||||
};
|
||||
|
||||
var client = _smtpClientFactory();
|
||||
try
|
||||
{
|
||||
client.Connect(config["EmailSettings:SmtpServer"], 465, true);
|
||||
client.Authenticate(config["EmailSettings:From"], config["EmailSettings:Password"]);
|
||||
client.Send(emailMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.Disconnect(true);
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/Application/Services/JwtService.cs
Normal file
65
src/Application/Services/JwtService.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Application.Interfaces;
|
||||
using Domain.Entities;
|
||||
using Domain.Interface;
|
||||
using Infrastructure.Utilities;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Application.Services;
|
||||
|
||||
public class JwtService(
|
||||
IConfiguration configuration,
|
||||
IUserRepository userRepository,
|
||||
IUnitOfWork unitOfWork) : IJwtService
|
||||
{
|
||||
public async Task<string> GenerateTokenAsync(User user)
|
||||
{
|
||||
var secretKey = configuration["Jwt:Key"];
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey!));
|
||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||
var roles = await userRepository.GetUserRolesByEmailAsync(user.Email);
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Email, user.Email),
|
||||
new("UserId", user.Id.ToString()),
|
||||
new("username", user.Username)
|
||||
};
|
||||
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
|
||||
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(claims),
|
||||
Expires = DateTime.UtcNow.AddDays(1), // set a token expiration time
|
||||
SigningCredentials = credentials,
|
||||
Issuer = configuration["Jwt:Issuer"],
|
||||
Audience = configuration["Jwt:Audience"]
|
||||
};
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
|
||||
return tokenHandler.WriteToken(securityToken);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAndSaveRefreshTokenAsync(User user)
|
||||
{
|
||||
var refreshToken = GenerateRefreshTokenHelper.GenerateRefreshToken();
|
||||
user.RefreshToken = refreshToken;
|
||||
user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(5);
|
||||
userRepository.Update(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
|
||||
public async Task<User?> ValidateRefreshTokenAsync(int userId, string refreshToken)
|
||||
{
|
||||
var user = await userRepository.GetUserByIdAsync(userId);
|
||||
if (user is null || user.RefreshToken != refreshToken
|
||||
|| user.RefreshTokenExpiryTime <= DateTime.UtcNow)
|
||||
return null;
|
||||
return user;
|
||||
}
|
||||
}
|
||||
163
src/Application/Services/UserService.cs
Normal file
163
src/Application/Services/UserService.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using Application.Common.Results;
|
||||
using Application.DTOs;
|
||||
using Application.Errors;
|
||||
using Application.Interfaces;
|
||||
using Application.Models;
|
||||
using Application.Validators;
|
||||
using Domain.Interface;
|
||||
|
||||
namespace Application.Services;
|
||||
|
||||
public class UserService(
|
||||
IUnitOfWork unitOfWork,
|
||||
UserUpdateRequestValidator userUpdateRequestValidator,
|
||||
IUserRepository userRepository,
|
||||
IUserRoleRepository userRoleRepository) : IUserService
|
||||
{
|
||||
public async Task<Result<PagedResult<UserDto>>> GetAsync(int pageNumber = 1, int pageSize = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
// fetch all users
|
||||
var users = await userRepository.GetAllAsync();
|
||||
var totalCount = users.Count;
|
||||
var pagedItems = users
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(user => new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Username = user.Username,
|
||||
LastLogin = user.LastLogin,
|
||||
Roles = user.UserRoles.Select(x => x.Role?.Name ?? "Unknown").ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var pageResult = new PagedResult<UserDto>
|
||||
{
|
||||
Items = pagedItems,
|
||||
TotalCount = totalCount,
|
||||
PageNumber = pageNumber,
|
||||
PageSize = pageSize
|
||||
};
|
||||
return Result.Success(pageResult);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> UpdateAsync(UserUpdateRequest userUpdateRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
// validate request
|
||||
var validationResult = await userUpdateRequestValidator.ValidateAsync(userUpdateRequest);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
var errors = validationResult.Errors.Select(a => a.ErrorMessage);
|
||||
return Result.Failure<string>(UserError.CreateInvalidUserUpdateRequestError(errors));
|
||||
}
|
||||
|
||||
// check if a user exists
|
||||
var user = await userRepository.GetByIdAsync(userUpdateRequest.Id);
|
||||
if (user == null) return Result.Failure<string>(UserError.UserNotFound);
|
||||
|
||||
// update user
|
||||
user.Username = userUpdateRequest.Username;
|
||||
user.Email = userUpdateRequest.Email;
|
||||
userRepository.Update(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return Result.Success("User updated successfully");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> DeleteAsync(int id, int currentUserId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Prevent users from deleting themselves
|
||||
if (id == currentUserId) return Result.Failure<string>(UserError.CannotDeleteYourself);
|
||||
|
||||
var user = await userRepository.GetByIdAsync(id);
|
||||
if (user == null) return Result.Failure<string>(UserError.UserNotFound);
|
||||
|
||||
userRepository.Delete(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return Result.Success("User deleted successfully");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<UserDto>> GetUserByIdAsync(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await userRepository.GetByIdAsync(id);
|
||||
if (user is null) return Result.Failure<UserDto>(UserError.UserNotFound);
|
||||
var userDetails = new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Username = user.Username,
|
||||
LastLogin = user.LastLogin,
|
||||
Roles = user.UserRoles.Select(x => x.Role?.Name ?? "Unknown").ToList()
|
||||
};
|
||||
return Result.Success(userDetails);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> AssignRoleAsync(AssingRoleRequest roleRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isUserHasRole = userRoleRepository.HasRoleAsync(roleRequest.UserId, roleRequest.RoleId);
|
||||
if (isUserHasRole.Result) return Result.Failure<string>(UserError.UserAlreadyHasRole);
|
||||
var result = await userRoleRepository.AddRoleAsync(roleRequest.UserId, roleRequest.RoleId);
|
||||
return result
|
||||
? Result.Success("Role assigned successfully")
|
||||
: Result.Failure<string>(UserError.FailedToAssignRole);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> RevokeRoleAsync(AssingRoleRequest roleRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isUserHasRole = userRoleRepository.HasRoleAsync(roleRequest.UserId, roleRequest.RoleId);
|
||||
if (!isUserHasRole.Result) return Result.Failure<string>(UserError.UserHasNoRole);
|
||||
|
||||
var result = await userRoleRepository.RemoveRoleAsync(roleRequest.UserId, roleRequest.RoleId);
|
||||
return result
|
||||
? Result.Success("Role revoked successfully")
|
||||
: Result.Failure<string>(UserError.FailedToRevokeRole);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Application/Validators/LoginRequestValidator.cs
Normal file
17
src/Application/Validators/LoginRequestValidator.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Application.Models;
|
||||
using FluentValidation;
|
||||
|
||||
namespace Application.Validators;
|
||||
|
||||
public class LoginRequestValidator : AbstractValidator<LoginRequest>
|
||||
{
|
||||
public LoginRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required.")
|
||||
.EmailAddress().WithMessage("Email is not valid.");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("Password is required.");
|
||||
}
|
||||
}
|
||||
29
src/Application/Validators/RegisterRequestValidator.cs
Normal file
29
src/Application/Validators/RegisterRequestValidator.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Application.Models;
|
||||
using FluentValidation;
|
||||
|
||||
namespace Application.Validators;
|
||||
|
||||
public class RegisterRequestValidator : AbstractValidator<RegisterRequest>
|
||||
{
|
||||
public RegisterRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("{PropertyName} is required.")
|
||||
.EmailAddress().WithMessage("{PropertyName} is not valid.");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("{PropertyName} is required.")
|
||||
.MinimumLength(10).WithMessage("{PropertyName} must be at least 10 characters long.")
|
||||
.Matches("[A-Z]").WithMessage("{PropertyName} must contain at least one uppercase letter.")
|
||||
.Matches("[a-z]").WithMessage("{PropertyName} must contain at least one lowercase letter.")
|
||||
.Matches("[0-9]").WithMessage("{PropertyName} must contain at least one digit.")
|
||||
.Matches("[^a-zA-Z0-9]").WithMessage("{PropertyName} must contain at least one special character.");
|
||||
|
||||
RuleFor(x => x.Username)
|
||||
.NotEmpty().WithMessage("{PropertyName} is required.")
|
||||
.MinimumLength(3).WithMessage("{PropertyName} must be at least 3 characters long.")
|
||||
.MaximumLength(20).WithMessage("{PropertyName} must not exceed 20 characters.")
|
||||
.Matches("^[a-zA-Z0-9_]*$")
|
||||
.WithMessage("{PropertyName} can only contain letters, numbers, and underscores.");
|
||||
}
|
||||
}
|
||||
21
src/Application/Validators/UserUpdateRequestValidator.cs
Normal file
21
src/Application/Validators/UserUpdateRequestValidator.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Application.Models;
|
||||
using FluentValidation;
|
||||
|
||||
namespace Application.Validators;
|
||||
|
||||
public class UserUpdateRequestValidator : AbstractValidator<UserUpdateRequest>
|
||||
{
|
||||
public UserUpdateRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Username)
|
||||
.NotEmpty().WithMessage("Username is required.")
|
||||
.MaximumLength(50).WithMessage(user => "Username must not exceed 50 characters.");
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required.")
|
||||
.EmailAddress().WithMessage("Email is invalid.");
|
||||
|
||||
RuleFor(x => x.Id)
|
||||
.NotEmpty().GreaterThan(0).WithMessage("Id is required.");
|
||||
}
|
||||
}
|
||||
9504
src/ClientApp/package-lock.json
generated
Normal file
9504
src/ClientApp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
src/Domain/Entities/Role.cs
Normal file
9
src/Domain/Entities/Role.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class Role
|
||||
{
|
||||
public required int Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
|
||||
public List<UserRole> UserRoles { get; set; } = [];
|
||||
}
|
||||
17
src/Domain/Entities/User.cs
Normal file
17
src/Domain/Entities/User.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public required string Username { get; set; }
|
||||
public required string Email { get; set; }
|
||||
public required string Password { get; set; }
|
||||
public DateTime LastLogin { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public DateTime? RefreshTokenExpiryTime { get; set; }
|
||||
public string? ResetPasswordToken { get; set; }
|
||||
public DateTime ResetPasswordTokenExpiryTime { get; set; }
|
||||
|
||||
public List<UserRole> UserRoles { get; set; } = [];
|
||||
|
||||
}
|
||||
10
src/Domain/Entities/UserRole.cs
Normal file
10
src/Domain/Entities/UserRole.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class UserRole
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
public User? User { get; set; }
|
||||
|
||||
public int RoleId { get; set; }
|
||||
public Role? Role { get; set; }
|
||||
}
|
||||
10
src/Domain/Interface/IGenericRepository.cs
Normal file
10
src/Domain/Interface/IGenericRepository.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Domain.Interface;
|
||||
|
||||
public interface IGenericRepository<TEntity> where TEntity : class
|
||||
{
|
||||
Task<TEntity?> GetByIdAsync(int id);
|
||||
Task<IReadOnlyList<TEntity>> GetAllAsync();
|
||||
Task<TEntity> AddAsync(TEntity entity);
|
||||
TEntity Update(TEntity entity);
|
||||
void Delete(TEntity entity);
|
||||
}
|
||||
7
src/Domain/Interface/IUnitOfWork.cs
Normal file
7
src/Domain/Interface/IUnitOfWork.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Domain.Interface;
|
||||
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
void Commit();
|
||||
Task CommitAsync();
|
||||
}
|
||||
12
src/Domain/Interface/IUserRepository.cs
Normal file
12
src/Domain/Interface/IUserRepository.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Domain.Entities;
|
||||
|
||||
namespace Domain.Interface;
|
||||
|
||||
public interface IUserRepository : IGenericRepository<User>
|
||||
{
|
||||
// extra implementation
|
||||
Task<User?> GetUserByEmailAsync(string email);
|
||||
Task<User?> GetUserByUsernameAsync(string username);
|
||||
Task<List<string>> GetUserRolesByEmailAsync(string email);
|
||||
Task<User?> GetUserByIdAsync(int id);
|
||||
}
|
||||
8
src/Domain/Interface/IUserRoleRepository.cs
Normal file
8
src/Domain/Interface/IUserRoleRepository.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Domain.Interface;
|
||||
|
||||
public interface IUserRoleRepository
|
||||
{
|
||||
Task<bool> AddRoleAsync(int userId, int roleId);
|
||||
Task<bool> RemoveRoleAsync(int userId, int roleId);
|
||||
Task<bool> HasRoleAsync(int userId, int roleId);
|
||||
}
|
||||
27
src/Infrastructure/Configuration/RoleConfiguration.cs
Normal file
27
src/Infrastructure/Configuration/RoleConfiguration.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Infrastructure.Configuration;
|
||||
|
||||
public class RoleConfiguration : IEntityTypeConfiguration<Role>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Role> builder)
|
||||
{
|
||||
builder.ToTable("Roles", "auth");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50);
|
||||
|
||||
builder.HasMany(x => x.UserRoles)
|
||||
.WithOne(x => x.Role)
|
||||
.HasForeignKey(x => x.RoleId);
|
||||
|
||||
builder.HasData(
|
||||
new Role { Id = 1, Name = "SuperAdmin" },
|
||||
new Role { Id = 2, Name = "Admin" },
|
||||
new Role { Id = 3, Name = "User" }
|
||||
);
|
||||
}
|
||||
}
|
||||
47
src/Infrastructure/Configuration/UserConfiguration.cs
Normal file
47
src/Infrastructure/Configuration/UserConfiguration.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Infrastructure.Configuration;
|
||||
|
||||
public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> builder)
|
||||
{
|
||||
builder.ToTable("Users", "auth");
|
||||
|
||||
builder.HasKey(k => k.Id);
|
||||
|
||||
builder.Property(x => x.Username)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
builder.Property(x => x.Password)
|
||||
.IsRequired()
|
||||
.HasMaxLength(250)
|
||||
.HasColumnType("varchar(250)");
|
||||
|
||||
builder.Property(x => x.Email)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)")
|
||||
.HasAnnotation("RegularExpression", @"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$");
|
||||
|
||||
builder.HasMany(x => x.UserRoles)
|
||||
.WithOne(x => x.User)
|
||||
.HasForeignKey(x => x.UserId);
|
||||
|
||||
builder.HasData(
|
||||
new User
|
||||
{
|
||||
Id = 1, Username = "Superadmin", Email = "superadmin@wenske-services-development.de",
|
||||
Password = "AQAAAAIAAYagAAAAEADJEu1s5qUJyP4gDUrBGyqSNtKU2IKBpZm0JqfyvOkJnqVeOHZBUrEhNr7IdQRDBQ=="
|
||||
},
|
||||
new User
|
||||
{
|
||||
Id = 2, Username = "Admin", Email = "admin@wenske-services-development.de",
|
||||
Password = "AQAAAAIAAYagAAAAEIOUiJUfMrM1Lpt4Ae3FLQOB/Bk6WHtndRAWUp132afVunMvRqkT6Hhh+27kkNW8YQ=="
|
||||
});
|
||||
}
|
||||
}
|
||||
25
src/Infrastructure/Configuration/UserRoleConfiguration.cs
Normal file
25
src/Infrastructure/Configuration/UserRoleConfiguration.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Infrastructure.Configuration;
|
||||
|
||||
public class UserRoleConfiguration : IEntityTypeConfiguration<UserRole>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserRole> builder)
|
||||
{
|
||||
builder.ToTable("UserRoles", "auth");
|
||||
builder.HasKey(x => new { x.UserId, x.RoleId });
|
||||
builder.HasOne(x => x.User)
|
||||
.WithMany(x => x.UserRoles)
|
||||
.HasForeignKey(x => x.UserId);
|
||||
|
||||
builder.HasOne(x => x.Role)
|
||||
.WithMany(x => x.UserRoles)
|
||||
.HasForeignKey(x => x.RoleId);
|
||||
|
||||
builder.HasData(
|
||||
new UserRole { UserId = 1, RoleId = 1 },
|
||||
new UserRole { UserId = 2, RoleId = 2 });
|
||||
}
|
||||
}
|
||||
16
src/Infrastructure/Context/ApplicationDbContext.cs
Normal file
16
src/Infrastructure/Context/ApplicationDbContext.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Infrastructure.Context;
|
||||
|
||||
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<Role> Roles { get; set; }
|
||||
public DbSet<UserRole> UserRoles { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
30
src/Infrastructure/Context/ApplicationDbContextFactory.cs
Normal file
30
src/Infrastructure/Context/ApplicationDbContextFactory.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Infrastructure.Utilities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Infrastructure.Context;
|
||||
|
||||
public static class DbContextOptionsFactory
|
||||
{
|
||||
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
|
||||
{
|
||||
public ApplicationDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
// Build the configuration manually
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../API"))
|
||||
.AddJsonFile("appsettings.json")
|
||||
.AddUserSecrets<ApplicationDbContextFactory>()
|
||||
.Build();
|
||||
// Read PostgreSQLSettings from configuration
|
||||
var postgreSqlSettings = configuration.GetRequiredSection("PostgreSQLSettings").Get<PostgreSqlSettings>();
|
||||
var connectionString = postgreSqlSettings?.ConnectionString;
|
||||
var optionBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
if (connectionString != null)
|
||||
optionBuilder.UseNpgsql(connectionString);
|
||||
|
||||
return new ApplicationDbContext(optionBuilder.Options);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Infrastructure/Extensions/ServiceCollectionExtensions.cs
Normal file
19
src/Infrastructure/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Domain.Interface;
|
||||
using Infrastructure.Repositories;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Infrastructure.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Register repositories and unit of work
|
||||
services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<IUserRoleRepository, UserRoleRepository>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,32 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>9c947c10-2373-4590-92a9-e5fe6b759c69</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.3.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.3.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
185
src/Infrastructure/Migrations/20260206104345_InitialCreate.Designer.cs
generated
Normal file
185
src/Infrastructure/Migrations/20260206104345_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,185 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Infrastructure.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260206104345_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Role", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Roles", "auth");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Name = "SuperAdmin"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Admin"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
Name = "User"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)")
|
||||
.HasAnnotation("RegularExpression", "^\\w+([-+.']\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$");
|
||||
|
||||
b.Property<DateTime>("LastLogin")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasMaxLength(250)
|
||||
.HasColumnType("varchar(250)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpiryTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ResetPasswordToken")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("ResetPasswordTokenExpiryTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users", "auth");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Email = "admin@rss.wenske-services-development.de",
|
||||
LastLogin = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Password = "AQAAAAIAAYagAAAAELBxAsYVTssn5taCQ7CMo+Mzn0i87Jt8ZXJTe7cgG5hfN3wDJzIkQaotyFhM/mQGaQ==",
|
||||
ResetPasswordTokenExpiryTime = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Username = "Superadmin"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Email = "info@rss.wenske-services-development.de",
|
||||
LastLogin = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Password = "AQAAAAIAAYagAAAAEP0NWiTTz20doMf/KL0WkBR+5roc5KTouMrfiHk2MMXOQn+E+C5Q4dqWD7PnNoxUmQ==",
|
||||
ResetPasswordTokenExpiryTime = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Username = "Admin"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.UserRole", b =>
|
||||
{
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("UserRoles", "auth");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
UserId = 1,
|
||||
RoleId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
UserId = 2,
|
||||
RoleId = 2
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.UserRole", b =>
|
||||
{
|
||||
b.HasOne("Domain.Entities.Role", "Role")
|
||||
.WithMany("UserRoles")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Domain.Entities.User", "User")
|
||||
.WithMany("UserRoles")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Role", b =>
|
||||
{
|
||||
b.Navigation("UserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("UserRoles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
136
src/Infrastructure/Migrations/20260206104345_InitialCreate.cs
Normal file
136
src/Infrastructure/Migrations/20260206104345_InitialCreate.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "auth");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Roles",
|
||||
schema: "auth",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Roles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
schema: "auth",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Username = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false),
|
||||
Email = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false),
|
||||
Password = table.Column<string>(type: "varchar(250)", maxLength: 250, nullable: false),
|
||||
LastLogin = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
RefreshToken = table.Column<string>(type: "text", nullable: true),
|
||||
RefreshTokenExpiryTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
ResetPasswordToken = table.Column<string>(type: "text", nullable: true),
|
||||
ResetPasswordTokenExpiryTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserRoles",
|
||||
schema: "auth",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<int>(type: "integer", nullable: false),
|
||||
RoleId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoles_Roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalSchema: "auth",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoles_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "auth",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
schema: "auth",
|
||||
table: "Roles",
|
||||
columns: new[] { "Id", "Name" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, "SuperAdmin" },
|
||||
{ 2, "Admin" },
|
||||
{ 3, "User" }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
schema: "auth",
|
||||
table: "Users",
|
||||
columns: new[] { "Id", "Email", "LastLogin", "Password", "RefreshToken", "RefreshTokenExpiryTime", "ResetPasswordToken", "ResetPasswordTokenExpiryTime", "Username" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, "admin@rss.wenske-services-development.de", new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), "AQAAAAIAAYagAAAAELBxAsYVTssn5taCQ7CMo+Mzn0i87Jt8ZXJTe7cgG5hfN3wDJzIkQaotyFhM/mQGaQ==", null, null, null, new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), "Superadmin" },
|
||||
{ 2, "info@rss.wenske-services-development.de", new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), "AQAAAAIAAYagAAAAEP0NWiTTz20doMf/KL0WkBR+5roc5KTouMrfiHk2MMXOQn+E+C5Q4dqWD7PnNoxUmQ==", null, null, null, new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), "Admin" }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
schema: "auth",
|
||||
table: "UserRoles",
|
||||
columns: new[] { "RoleId", "UserId" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, 1 },
|
||||
{ 2, 2 }
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRoles_RoleId",
|
||||
schema: "auth",
|
||||
table: "UserRoles",
|
||||
column: "RoleId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserRoles",
|
||||
schema: "auth");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Roles",
|
||||
schema: "auth");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users",
|
||||
schema: "auth");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Infrastructure.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Role", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Roles", "auth");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Name = "SuperAdmin"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Admin"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
Name = "User"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)")
|
||||
.HasAnnotation("RegularExpression", "^\\w+([-+.']\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$");
|
||||
|
||||
b.Property<DateTime>("LastLogin")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasMaxLength(250)
|
||||
.HasColumnType("varchar(250)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpiryTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ResetPasswordToken")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("ResetPasswordTokenExpiryTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users", "auth");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Email = "admin@rss.wenske-services-development.de",
|
||||
LastLogin = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Password = "AQAAAAIAAYagAAAAELBxAsYVTssn5taCQ7CMo+Mzn0i87Jt8ZXJTe7cgG5hfN3wDJzIkQaotyFhM/mQGaQ==",
|
||||
ResetPasswordTokenExpiryTime = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Username = "Superadmin"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Email = "info@rss.wenske-services-development.de",
|
||||
LastLogin = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Password = "AQAAAAIAAYagAAAAEP0NWiTTz20doMf/KL0WkBR+5roc5KTouMrfiHk2MMXOQn+E+C5Q4dqWD7PnNoxUmQ==",
|
||||
ResetPasswordTokenExpiryTime = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Username = "Admin"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.UserRole", b =>
|
||||
{
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("UserRoles", "auth");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
UserId = 1,
|
||||
RoleId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
UserId = 2,
|
||||
RoleId = 2
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.UserRole", b =>
|
||||
{
|
||||
b.HasOne("Domain.Entities.Role", "Role")
|
||||
.WithMany("UserRoles")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Domain.Entities.User", "User")
|
||||
.WithMany("UserRoles")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Role", b =>
|
||||
{
|
||||
b.Navigation("UserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("UserRoles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/Infrastructure/Repositories/GenericRepository.cs
Normal file
38
src/Infrastructure/Repositories/GenericRepository.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Domain.Interface;
|
||||
using Infrastructure.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
public class GenericRepository<TEntity>(ApplicationDbContext applicationDbContext)
|
||||
: IGenericRepository<TEntity> where TEntity : class
|
||||
{
|
||||
protected readonly DbSet<TEntity> DbSet = applicationDbContext.Set<TEntity>();
|
||||
|
||||
public async Task<TEntity> AddAsync(TEntity entity)
|
||||
{
|
||||
var entry = await DbSet.AddAsync(entity);
|
||||
return entry.Entity;
|
||||
}
|
||||
|
||||
public void Delete(TEntity entity)
|
||||
{
|
||||
DbSet.Remove(entity);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TEntity>> GetAllAsync()
|
||||
{
|
||||
return await DbSet.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<TEntity?> GetByIdAsync(int id)
|
||||
{
|
||||
return await DbSet.FindAsync(id);
|
||||
}
|
||||
|
||||
public TEntity Update(TEntity entity)
|
||||
{
|
||||
var entry = DbSet.Update(entity);
|
||||
return entry.Entity;
|
||||
}
|
||||
}
|
||||
17
src/Infrastructure/Repositories/UnitOfWork.cs
Normal file
17
src/Infrastructure/Repositories/UnitOfWork.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Domain.Interface;
|
||||
using Infrastructure.Context;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
public class UnitOfWork(ApplicationDbContext context) : IUnitOfWork
|
||||
{
|
||||
public void Commit()
|
||||
{
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
public async Task CommitAsync()
|
||||
{
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
34
src/Infrastructure/Repositories/UserRepository.cs
Normal file
34
src/Infrastructure/Repositories/UserRepository.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Interface;
|
||||
using Infrastructure.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
public class UserRepository(ApplicationDbContext applicationDbContext)
|
||||
: GenericRepository<User>(applicationDbContext), IUserRepository
|
||||
{
|
||||
public async Task<User?> GetUserByEmailAsync(string email)
|
||||
{
|
||||
return await DbSet.FirstOrDefaultAsync(u => u.Email == email);
|
||||
}
|
||||
|
||||
public async Task<User?> GetUserByUsernameAsync(string username)
|
||||
{
|
||||
return await DbSet.FirstOrDefaultAsync(u => u.Username == username);
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetUserRolesByEmailAsync(string email)
|
||||
{
|
||||
return await DbSet
|
||||
.Where(ur => ur.Email == email)
|
||||
.SelectMany(ur => ur.UserRoles)
|
||||
.Select(ur => ur.Role!.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<User?> GetUserByIdAsync(int id)
|
||||
{
|
||||
return await DbSet.FindAsync(id);
|
||||
}
|
||||
}
|
||||
62
src/Infrastructure/Repositories/UserRoleRepository.cs
Normal file
62
src/Infrastructure/Repositories/UserRoleRepository.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Interface;
|
||||
using Infrastructure.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Infrastructure.Repositories;
|
||||
|
||||
public class UserRoleRepository(ApplicationDbContext applicationDbContext) : IUserRoleRepository
|
||||
{
|
||||
public async Task<bool> AddRoleAsync(int userId, int roleId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userRole = new UserRole
|
||||
{
|
||||
UserId = userId,
|
||||
RoleId = roleId
|
||||
};
|
||||
await applicationDbContext.UserRoles.AddAsync(userRole);
|
||||
var result = await applicationDbContext.SaveChangesAsync() > 0;
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveRoleAsync(int userId, int roleId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userRole =
|
||||
await applicationDbContext.UserRoles.FirstOrDefaultAsync(u => u.UserId == userId && u.RoleId == roleId);
|
||||
if (userRole is null) return false;
|
||||
applicationDbContext.UserRoles.Remove(userRole);
|
||||
var result = await applicationDbContext.SaveChangesAsync() > 0;
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> HasRoleAsync(int userId, int roleId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isUserHasRole =
|
||||
await applicationDbContext.UserRoles.AnyAsync(u => u.UserId == userId && u.RoleId == roleId);
|
||||
return isUserHasRole;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/Infrastructure/Utilities/EmailBody.cs
Normal file
72
src/Infrastructure/Utilities/EmailBody.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
namespace Infrastructure.Utilities;
|
||||
|
||||
public static class EmailBody
|
||||
{
|
||||
public static string EmailStringBody(string email, string emailToken)
|
||||
{
|
||||
// URL-encode parameters for security
|
||||
var encodedEmail = Uri.EscapeDataString(email);
|
||||
var encodedToken = Uri.EscapeDataString(emailToken);
|
||||
|
||||
return $@"<!DOCTYPE html>
|
||||
<html lang=""en"">
|
||||
<head>
|
||||
<meta charset=""UTF-8"">
|
||||
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||
<title>Reset Your Password</title>
|
||||
</head>
|
||||
<body style=""margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f4;"">
|
||||
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""background-color: #f4f4f4; padding: 20px 0;"">
|
||||
<tr>
|
||||
<td align=""center"">
|
||||
<table width=""600"" cellpadding=""0"" cellspacing=""0"" style=""background: linear-gradient(#4d82c3, #605d5d, #fda400) no-repeat; border-radius: 10px; overflow: hidden; max-width: 100%;"">
|
||||
<tr>
|
||||
<td style=""padding: 40px 30px; background-color: rgba(255, 255, 255, 0.95); margin: 20px;"">
|
||||
<h1 style=""color: #333; text-align: center; margin: 0 0 20px 0; font-size: 28px;"">Reset Your Password</h1>
|
||||
<hr style=""border: none; border-top: 2px solid #4d82c3; margin: 20px 0;"">
|
||||
|
||||
<p style=""color: #555; text-align: center; font-size: 16px; line-height: 1.6; margin: 20px 0;"">
|
||||
You're receiving this email because you requested a password reset for your RssReader account.
|
||||
</p>
|
||||
|
||||
<p style=""color: #555; font-size: 16px; line-height: 1.6; margin: 20px 0;"">
|
||||
Please click the button below to choose a new password:
|
||||
</p>
|
||||
|
||||
<table width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""margin: 30px 0;"">
|
||||
<tr>
|
||||
<td align=""center"">
|
||||
<a href=""https://rss.wenske-services-development.de/reset?email={encodedEmail}&code={encodedToken}""
|
||||
target=""_blank""
|
||||
style=""background-color: #4d82c3;
|
||||
color: #ffffff;
|
||||
padding: 15px 40px;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
display: inline-block;"">
|
||||
Reset Password
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p style=""color: #999; font-size: 14px; line-height: 1.6; margin: 30px 0 0 0; border-top: 1px solid #e0e0e0; padding-top: 20px;"">
|
||||
If you didn't request a password reset, you can safely ignore this email.
|
||||
</p>
|
||||
|
||||
<p style=""color: #555; font-size: 16px; margin: 30px 0 0 0;"">
|
||||
Kind regards,<br>
|
||||
<strong>The RssReader Team</strong>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>";
|
||||
}
|
||||
}
|
||||
14
src/Infrastructure/Utilities/GenerateRefreshTokenHelper.cs
Normal file
14
src/Infrastructure/Utilities/GenerateRefreshTokenHelper.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Infrastructure.Utilities;
|
||||
|
||||
public abstract class GenerateRefreshTokenHelper
|
||||
{
|
||||
public static string GenerateRefreshToken()
|
||||
{
|
||||
var randomNumber = new byte[32];
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
rng.GetBytes(randomNumber);
|
||||
return Convert.ToBase64String(randomNumber);
|
||||
}
|
||||
}
|
||||
11
src/Infrastructure/Utilities/PostgreSqlSettings.cs
Normal file
11
src/Infrastructure/Utilities/PostgreSqlSettings.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Infrastructure.Utilities;
|
||||
|
||||
public class PostgreSqlSettings
|
||||
{
|
||||
public string? Host { get; set; }
|
||||
public string? Database { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? Password { get; set; }
|
||||
|
||||
public string ConnectionString => $"Host={Host};Database={Database};Username={Username};Password={Password};";
|
||||
}
|
||||
Reference in New Issue
Block a user