initial commit

This commit is contained in:
cuqmbr 2025-04-29 23:51:19 +03:00
commit d694573c19
Signed by: cuqmbr
GPG Key ID: 0AA446880C766199
201 changed files with 15211 additions and 0 deletions

484
.gitignore vendored Normal file
View File

@ -0,0 +1,484 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from `dotnet new gitignore`
# dotenv files
.env
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
.idea/
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# Vim temporary swap files
*.swp

41
Dockerfile Normal file
View File

@ -0,0 +1,41 @@
ARG BUILD_PATH_ARG=/build
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ENV SOURCE_PATH=/source
ARG BUILD_PATH_ARG
ENV BUILD_PATH=${BUILD_PATH_ARG}
ADD ./src ${SOURCE_PATH}
WORKDIR ${SOURCE_PATH}
RUN dotnet publish HttpApi \
--output ${BUILD_PATH} --no-self-contained \
-p:PublishSingleFile=true -p:IsAotCompatible=true
RUN rm -Rf ${BUILD_PATH}/*pdb
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
ARG BUILD_PATH_ARG
ENV BUILD_PATH=${BUILD_PATH_ARG}
ARG APP_PATH_ARG=/app
ENV APP_PATH=${APP_PATH_ARG}
WORKDIR ${APP_PATH}
COPY --from=build ${BUILD_PATH} .
EXPOSE 8080/tcp
HEALTHCHECK CMD curl --fail http://localhost:8080/health || exit
ENTRYPOINT ${APP_PATH}/HttpApi

64
TravelGuide.sln Normal file
View File

@ -0,0 +1,64 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "src\Domain\Domain.csproj", "{958720B9-290A-4C45-93D7-464547EC05FA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "src\Application\Application.csproj", "{6FA62100-F370-4985-8133-7DBB0EEB27AC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Persistence", "src\Persistence\Persistence.csproj", "{B9425855-6C99-4E4B-92F8-049B342F76B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{266B11B4-6838-4D17-A277-EDB40A9F1A5E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpApi", "src\HttpApi\HttpApi.csproj", "{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity", "src\Identity\Identity.csproj", "{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Configuration", "src\Configuration\Configuration.csproj", "{1DCFA4EE-A545-42FE-A3BC-A606D2961298}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.IntegrationTests", "tst\Application.IntegrationTests\Application.IntegrationTests.csproj", "{B52B8651-10B8-488D-8ACF-9C4499F8A723}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{958720B9-290A-4C45-93D7-464547EC05FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{958720B9-290A-4C45-93D7-464547EC05FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{958720B9-290A-4C45-93D7-464547EC05FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{958720B9-290A-4C45-93D7-464547EC05FA}.Release|Any CPU.Build.0 = Release|Any CPU
{6FA62100-F370-4985-8133-7DBB0EEB27AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FA62100-F370-4985-8133-7DBB0EEB27AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FA62100-F370-4985-8133-7DBB0EEB27AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FA62100-F370-4985-8133-7DBB0EEB27AC}.Release|Any CPU.Build.0 = Release|Any CPU
{B9425855-6C99-4E4B-92F8-049B342F76B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9425855-6C99-4E4B-92F8-049B342F76B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9425855-6C99-4E4B-92F8-049B342F76B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9425855-6C99-4E4B-92F8-049B342F76B1}.Release|Any CPU.Build.0 = Release|Any CPU
{266B11B4-6838-4D17-A277-EDB40A9F1A5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{266B11B4-6838-4D17-A277-EDB40A9F1A5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{266B11B4-6838-4D17-A277-EDB40A9F1A5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{266B11B4-6838-4D17-A277-EDB40A9F1A5E}.Release|Any CPU.Build.0 = Release|Any CPU
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.Build.0 = Release|Any CPU
{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Release|Any CPU.Build.0 = Release|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Release|Any CPU.Build.0 = Release|Any CPU
{B52B8651-10B8-488D-8ACF-9C4499F8A723}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B52B8651-10B8-488D-8ACF-9C4499F8A723}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B52B8651-10B8-488D-8ACF-9C4499F8A723}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B52B8651-10B8-488D-8ACF-9C4499F8A723}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

6
global.json Normal file
View File

@ -0,0 +1,6 @@
{
"sdk": {
"version": "9.0.104",
"rollForward": "latestMinor"
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.Localizer.Json" Version="1.0.1" />
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="MediatR.Behaviors.Authorization" Version="12.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.2" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\**" WithCulture="false" />
</ItemGroup>
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,10 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public record RegisterCommand : IRequest
{
public string Email { get; set; }
public string Password { get; set; }
}

View File

@ -0,0 +1,21 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public class RegisterCommandHandler : IRequestHandler<RegisterCommand>
{
private readonly AuthenticationService _authenticationService;
public RegisterCommandHandler(AuthenticationService authenticationService)
{
_authenticationService = authenticationService;
}
public async Task Handle(
RegisterCommand request, CancellationToken cancellationToken)
{
await _authenticationService.RegisterAsync(
request.Email, request.Password, cancellationToken);
}
}

View File

@ -0,0 +1,31 @@
using FluentValidation;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public class RegisterCommandValidator : AbstractValidator<RegisterCommand>
{
public RegisterCommandValidator()
{
RuleFor(v => v.Email)
.NotEmpty()
.WithMessage("Email address is required.")
.Matches(@"\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b")
.WithMessage("Email address is invalid.");
RuleFor(v => v.Password)
.NotEmpty()
.WithMessage("Password is required.")
.MinimumLength(8)
.WithMessage("Password must be at least 8 characters long.")
.MaximumLength(64)
.WithMessage("Password must be at most 64 characters long.")
.Matches(@"(?=.*[A-Z]).*")
.WithMessage("Password must contain at least one uppercase letter.")
.Matches(@"(?=.*[a-z]).*")
.WithMessage("Password must contain at least one lowercase letter.")
.Matches(@"(?=.*[\d]).*")
.WithMessage("Password must contain at least one digit.")
.Matches(@"(?=.*[!@#$%^&*()]).*")
.WithMessage("Password must contain at least one of the following special charactters: !@#$%^&*().");
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken;
public record RenewAccessTokenCommand : IRequest<TokensModel>
{
public string RefreshToken { get; set; }
}

View File

@ -0,0 +1,24 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken;
public class RenewAccessTokenCommandAuthorizer :
AbstractRequestAuthorizer<RenewAccessTokenCommand>
{
private readonly SessionUserService _sessionUserService;
public RenewAccessTokenCommandAuthorizer(SessionUserService currentUserService)
{
_sessionUserService = currentUserService;
}
public override void BuildPolicy(RenewAccessTokenCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
}
}

View File

@ -0,0 +1,22 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken;
public class RenewAccessTokenCommandHandler :
IRequestHandler<RenewAccessTokenCommand, TokensModel>
{
private readonly AuthenticationService _authenticationService;
public RenewAccessTokenCommandHandler(AuthenticationService authenticationService)
{
_authenticationService = authenticationService;
}
public async Task<TokensModel> Handle(
RenewAccessTokenCommand request, CancellationToken cancellationToken)
{
return await _authenticationService.RenewAccessTokenAsync(
request.RefreshToken, cancellationToken);
}
}

View File

@ -0,0 +1,12 @@
using FluentValidation;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken;
public class RenewAccessTokenCommandValidator :
AbstractValidator<RenewAccessTokenCommand>
{
public RenewAccessTokenCommandValidator()
{
RuleFor(v => v.RefreshToken).NotEmpty();
}
}

View File

@ -0,0 +1,6 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RenewAccessTokenWithCookie;
public record RenewAccessTokenWithCookieCommand : IRequest<TokensModel> { }

View File

@ -0,0 +1,26 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RenewAccessTokenWithCookie;
public class RenewAccessTokenWithCookieCommandAuthorizer :
AbstractRequestAuthorizer<RenewAccessTokenWithCookieCommand>
{
private readonly SessionUserService _sessionUserService;
public RenewAccessTokenWithCookieCommandAuthorizer(
SessionUserService currentUserService)
{
_sessionUserService = currentUserService;
}
public override void BuildPolicy(RenewAccessTokenWithCookieCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
}
}

View File

@ -0,0 +1,28 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RenewAccessTokenWithCookie;
public class RenewAccessTokenWithCookieCommandHandler :
IRequestHandler<RenewAccessTokenWithCookieCommand, TokensModel>
{
private readonly AuthenticationService _authenticationService;
private readonly SessionUserService _sessionUserService;
public RenewAccessTokenWithCookieCommandHandler(
AuthenticationService authenticationService,
SessionUserService sessionUserService)
{
_authenticationService = authenticationService;
_sessionUserService = sessionUserService;
}
public async Task<TokensModel> Handle(
RenewAccessTokenWithCookieCommand request,
CancellationToken cancellationToken)
{
return await _authenticationService.RenewAccessTokenAsync(
_sessionUserService.RefreshToken, cancellationToken);
}
}

View File

@ -0,0 +1,10 @@
using FluentValidation;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RenewAccessTokenWithCookie;
public class RenewAccessTokenWithCookieCommandValidator :
AbstractValidator<RenewAccessTokenWithCookieCommand>
{
public RenewAccessTokenWithCookieCommandValidator() { }
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken;
public record RevokeRefreshTokenCommand : IRequest
{
public string RefreshToken { get; set; }
}

View File

@ -0,0 +1,24 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken;
public class RevokeRefreshTokenCommandAuthorizer :
AbstractRequestAuthorizer<RevokeRefreshTokenCommand>
{
private readonly SessionUserService _sessionUserService;
public RevokeRefreshTokenCommandAuthorizer(SessionUserService currentUserService)
{
_sessionUserService = currentUserService;
}
public override void BuildPolicy(RevokeRefreshTokenCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
}
}

View File

@ -0,0 +1,22 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken;
public class RevokeRefreshTokenCommandHandler :
IRequestHandler<RevokeRefreshTokenCommand>
{
private readonly AuthenticationService _authenticationService;
public RevokeRefreshTokenCommandHandler(AuthenticationService authenticationService)
{
_authenticationService = authenticationService;
}
public async Task Handle(
RevokeRefreshTokenCommand request, CancellationToken cancellationToken)
{
await _authenticationService.RevokeRefreshTokenAsync(
request.RefreshToken, cancellationToken);
}
}

View File

@ -0,0 +1,12 @@
using FluentValidation;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken;
public class RevokeRefreshTokenCommandValidator :
AbstractValidator<RevokeRefreshTokenCommand>
{
public RevokeRefreshTokenCommandValidator()
{
RuleFor(v => v.RefreshToken).NotEmpty();
}
}

View File

@ -0,0 +1,6 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RevokeRefreshTokenWithCookie;
public record RevokeRefreshTokenWithCookieCommand : IRequest { }

View File

@ -0,0 +1,26 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RevokeRefreshTokenWithCookie;
public class RevokeRefreshTokenWithCookieCommandAuthorizer :
AbstractRequestAuthorizer<RevokeRefreshTokenWithCookieCommand>
{
private readonly SessionUserService _sessionUserService;
public RevokeRefreshTokenWithCookieCommandAuthorizer(
SessionUserService currentUserService)
{
_sessionUserService = currentUserService;
}
public override void BuildPolicy(RevokeRefreshTokenWithCookieCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
}
}

View File

@ -0,0 +1,28 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RevokeRefreshTokenWithCookie;
public class RevokeRefreshTokenWithCookieCommandHandler :
IRequestHandler<RevokeRefreshTokenWithCookieCommand>
{
private readonly AuthenticationService _authenticationService;
private readonly SessionUserService _sessionUserService;
public RevokeRefreshTokenWithCookieCommandHandler(
AuthenticationService authenticationService,
SessionUserService sessionUserService)
{
_authenticationService = authenticationService;
_sessionUserService = sessionUserService;
}
public async Task Handle(
RevokeRefreshTokenWithCookieCommand request,
CancellationToken cancellationToken)
{
await _authenticationService.RevokeRefreshTokenAsync(
_sessionUserService.RefreshToken, cancellationToken);
}
}

View File

@ -0,0 +1,10 @@
using FluentValidation;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RevokeRefreshTokenWithCookie;
public class RevokeRefreshTokenWithCookieCommandValidator :
AbstractValidator<RevokeRefreshTokenWithCookieCommand>
{
public RevokeRefreshTokenWithCookieCommandValidator() { }
}

View File

@ -0,0 +1,10 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public record LoginQuery : IRequest<TokensModel>
{
public string Email { get; set; }
public string Password { get; set; }
}

View File

@ -0,0 +1,21 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public class LoginQueryHandler : IRequestHandler<LoginQuery, TokensModel>
{
private readonly AuthenticationService _authenticationService;
public LoginQueryHandler(AuthenticationService authenticationService)
{
_authenticationService = authenticationService;
}
public async Task<TokensModel> Handle(
LoginQuery request, CancellationToken cancellationToken)
{
return await _authenticationService.LoginAsync(
request.Email, request.Password, cancellationToken);
}
}

View File

@ -0,0 +1,15 @@
using FluentValidation;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public class LoginQueryValidator : AbstractValidator<LoginQuery>
{
public LoginQueryValidator()
{
RuleFor(v => v.Email)
.NotEmpty().WithMessage("Email address is required.");
RuleFor(v => v.Password)
.NotEmpty().WithMessage("Password is required.");
}
}

View File

@ -0,0 +1,14 @@
namespace cuqmbr.TravelGuide.Application.Authenticaion;
public sealed class TokensModel
{
public TokensModel(string accessToken, string refreshToken)
{
AccessToken = accessToken;
RefreshToken = refreshToken;
}
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}

View File

@ -0,0 +1,13 @@
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using MediatR.Behaviors.Authorization;
using MediatR.Behaviors.Authorization.Interfaces;
namespace cuqmbr.TravelGuide.Application.Common.Authorization;
public class ForbiddenUnauthorizedResultHandler : IUnauthorizedResultHandler
{
public Task<TResponse> Invoke<TResponse>(AuthorizationResult result)
{
throw new ForbiddenException(result.FailureMessage);
}
}

View File

@ -0,0 +1,27 @@
// using cuqmbr.TravelGuide.Application.Common.Exceptions;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Common.Authorization;
public class MustBeAuthenticatedRequirement : IAuthorizationRequirement
{
public required bool IsAuthenticated { get; init; } = default!;
class MustBeAuthenticatedRequirementHandler :
IAuthorizationHandler<MustBeAuthenticatedRequirement>
{
public Task<AuthorizationResult> Handle(
MustBeAuthenticatedRequirement request,
CancellationToken cancellationToken)
{
if (!request.IsAuthenticated)
{
return Task.FromResult(AuthorizationResult.Fail());
// TODO: Remove UnAuthorizedException, isn't used
// throw new UnAuthorizedException();
}
return Task.FromResult(AuthorizationResult.Succeed());
}
}
}

View File

@ -0,0 +1,41 @@
using MediatR.Behaviors.Authorization;
using Microsoft.Extensions.Localization;
using cuqmbr.TravelGuide.Application.Common.Models;
namespace cuqmbr.TravelGuide.Application.Common.Authorization;
public class MustBeInRolesRequirement : IAuthorizationRequirement
{
public ICollection<IdentityRole> UserRoles { get; init; }
public ICollection<IdentityRole> RequiredRoles { get; init; }
class MustBeInRolesRequirementHandler :
IAuthorizationHandler<MustBeInRolesRequirement>
{
private readonly IStringLocalizer _localizer;
public MustBeInRolesRequirementHandler(IStringLocalizer localizer)
{
_localizer = localizer;
}
public Task<AuthorizationResult> Handle(
MustBeInRolesRequirement request,
CancellationToken cancellationToken)
{
var isUserInRequiredRoles =
request.UserRoles?.Any(ur => request.RequiredRoles.Contains(ur))
?? false;
if (!isUserInRequiredRoles)
{
// TODO: Make message visible in api response
return Task.FromResult(AuthorizationResult.Fail(
"You must be in the following roles: " +
$"{String.Join(", ", request.RequiredRoles)}."));
}
return Task.FromResult(AuthorizationResult.Succeed());
}
}
}

View File

@ -0,0 +1,46 @@
using System.Diagnostics;
using MediatR;
using Microsoft.Extensions.Logging;
namespace cuqmbr.TravelGuide.Application.Common.Behaviours;
public class LoggingBehaviour<TRequest, TResponse> :
IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly ILogger _logger;
private readonly Stopwatch _stopWatch;
public LoggingBehaviour(ILogger<TRequest> logger)
{
_logger = logger;
_stopWatch = new Stopwatch();
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
_logger.LogDebug("Started handling MediatR request.");
_stopWatch.Start();
var response = await next();
_stopWatch.Stop();
_logger.LogInformation(
"MediatR request handled in {Duration}ms.",
_stopWatch.ElapsedMilliseconds);
if (_stopWatch.ElapsedMilliseconds > 500)
{
_logger.LogWarning(
"MediatR request was handled slowly ({Duration}ms).",
_stopWatch.ElapsedMilliseconds);
}
return response;
}
}

View File

@ -0,0 +1,42 @@
using FluentValidation;
using MediatR;
using ValidationException =
cuqmbr.TravelGuide.Application.Common.Exceptions.ValidationException;
namespace cuqmbr.TravelGuide.Application.Common.Behaviours;
public class ValidationBehaviour<TRequest, TResponse> :
IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
{
throw new ValidationException(failures);
}
}
return await next();
}
}

View File

@ -0,0 +1,11 @@
namespace cuqmbr.TravelGuide.Application.Common.Exceptions;
public class AuthenticationException : Exception
{
public AuthenticationException()
: base() { }
public AuthenticationException(string message)
: base(message) { }
}

View File

@ -0,0 +1,11 @@
namespace cuqmbr.TravelGuide.Application.Common.Exceptions;
public class DuplicateEntityException : Exception
{
public DuplicateEntityException()
: base() { }
public DuplicateEntityException(string message)
: base(message) { }
}

View File

@ -0,0 +1,11 @@
namespace cuqmbr.TravelGuide.Application.Common.Exceptions;
public class ForbiddenException : Exception
{
public ForbiddenException()
: base() { }
public ForbiddenException(string message)
: base(message) { }
}

View File

@ -0,0 +1,11 @@
namespace cuqmbr.TravelGuide.Application.Common.Exceptions;
public class LoginException : Exception
{
public LoginException()
: base() { }
public LoginException(string message)
: base(message) { }
}

View File

@ -0,0 +1,11 @@
namespace cuqmbr.TravelGuide.Application.Common.Exceptions;
public class NotFoundException : Exception
{
public NotFoundException()
: base() { }
public NotFoundException(string message)
: base(message) { }
}

View File

@ -0,0 +1,11 @@
namespace cuqmbr.TravelGuide.Application.Common.Exceptions;
public class RegistrationException : Exception
{
public RegistrationException()
: base() { }
public RegistrationException(string message)
: base(message) { }
}

View File

@ -0,0 +1,10 @@
namespace cuqmbr.TravelGuide.Application.Common.Exceptions;
public class UnAuthorizedException : Exception
{
public UnAuthorizedException()
: base() { }
public UnAuthorizedException(string message)
: base(message) { }
}

View File

@ -0,0 +1,23 @@
using FluentValidation.Results;
namespace cuqmbr.TravelGuide.Application.Common.Exceptions;
public class ValidationException : Exception
{
public ValidationException()
: base("One or more validation failures have occurred.")
{
Errors = new Dictionary<string, string[]>();
}
public ValidationException(IEnumerable<ValidationFailure> failures)
: this()
{
Errors = failures
.GroupBy(f => f.PropertyName, f => f.ErrorMessage)
.ToDictionary(fg => fg.Key, fg => fg.ToArray());
}
public IDictionary<string, string[]> Errors { get; }
}

View File

@ -0,0 +1,73 @@
using System.Linq.Dynamic.Core;
using System.Reflection;
using System.Text;
namespace cuqmbr.TravelGuide.Application.Common.Extensions;
public static class QueryableExtension<T>
{
public static IQueryable<T> ApplySort(
IQueryable<T> entities, string? orderQueryString)
{
if (!entities.Any() || String.IsNullOrWhiteSpace(orderQueryString))
{
return entities;
}
var orderParameters = orderQueryString.Trim().Split(",");
var queryPropertyTypes = typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToList();
var queryPropertyNames = queryPropertyTypes.ConvertAll(o => o.Name);
var orderQueryBuilder = new StringBuilder();
foreach (var parameter in orderParameters)
{
if (string.IsNullOrWhiteSpace(parameter))
{
continue;
}
var queryPropertyName =
parameter[0] == '-' ||
parameter[0] == '+'
? parameter.Substring(1)
: parameter;
var propertyName = queryPropertyNames
.FirstOrDefault(pn =>
pn.Equals(
queryPropertyName,
StringComparison.InvariantCultureIgnoreCase));
if (propertyName == null)
{
continue;
}
var sortingOrder =
parameter[0] == '-'
? "descending"
: "ascending";
orderQueryBuilder.Append($"{propertyName} {sortingOrder}, ");
}
var orderQuery = orderQueryBuilder.ToString()
.TrimEnd(',', ' ');
if (String.IsNullOrWhiteSpace(orderQuery))
{
return entities;
}
return entities.OrderBy(orderQuery);
}
}

View File

@ -0,0 +1,51 @@
using System.Linq.Expressions;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface BaseRepository<TEntity>
where TEntity : EntityBase
{
Task<TEntity> AddOneAsync(
TEntity entity,
CancellationToken cancellationToken);
Task<TEntity?> GetOneAsync(
Expression<Func<TEntity, bool>> predicate,
CancellationToken cancellationToken);
Task<TEntity?> GetOneAsync(
Expression<Func<TEntity, bool>> predicate,
Expression<Func<TEntity, object>> includeSelector,
CancellationToken cancellationToken);
Task<PaginatedList<TEntity>> GetPageAsync(
int pageNumber, int pageSize,
CancellationToken cancellationToken);
Task<PaginatedList<TEntity>> GetPageAsync(
Expression<Func<TEntity, object>> includeSelector,
int pageNumber, int pageSize,
CancellationToken cancellationToken);
Task<PaginatedList<TEntity>> GetPageAsync(
Expression<Func<TEntity, bool>> predicate,
int pageNumber, int pageSize,
CancellationToken cancellationToken);
Task<PaginatedList<TEntity>> GetPageAsync(
Expression<Func<TEntity, bool>> predicate,
Expression<Func<TEntity, object>> includeSelector,
int pageNumber, int pageSize,
CancellationToken cancellationToken);
Task<TEntity> UpdateOneAsync(
TEntity entity,
CancellationToken cancellationToken);
Task DeleteOneAsync(
TEntity entity,
CancellationToken cancellationToken);
}

View File

@ -0,0 +1,6 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface CountryRepository : BaseRepository<Country> { }

View File

@ -0,0 +1,6 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface RegionRepository : BaseRepository<Region> { }

View File

@ -0,0 +1,12 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
public interface UnitOfWork : IDisposable
{
CountryRepository CountryRepository { get; }
RegionRepository RegionRepository { get; }
int Save();
Task<int> SaveAsync(CancellationToken cancellationToken);
}

View File

@ -0,0 +1,18 @@
using cuqmbr.TravelGuide.Application.Authenticaion;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
public interface AuthenticationService
{
Task RegisterAsync(string email, string password,
CancellationToken cancellationToken);
Task<TokensModel> LoginAsync(string email, string password,
CancellationToken cancellationToken);
Task<TokensModel> RenewAccessTokenAsync(string refreshToken,
CancellationToken cancellationToken);
Task RevokeRefreshTokenAsync(string refreshToken,
CancellationToken cancellationToken);
}

View File

@ -0,0 +1,8 @@
using System.Globalization;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
public interface CultureService
{
public CultureInfo Culture { get; }
}

View File

@ -0,0 +1,22 @@
using cuqmbr.TravelGuide.Application.Common.Models;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
public interface SessionUserService
{
public int? Id { get; }
public Guid? Uuid { get; }
public string? Email { get; }
public ICollection<IdentityRole> Roles { get; }
public bool IsAuthenticated => Id != null;
public string? AccessToken { get; }
public string? RefreshToken { get; }
}

View File

@ -0,0 +1,6 @@
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
public interface TimeZoneService
{
public TimeZoneInfo TimeZone { get; }
}

View File

@ -0,0 +1,6 @@
namespace cuqmbr.TravelGuide.Application.Common.Mappings;
public interface IMapFrom<T>
{
void Mapping(MappingProfile profile) => profile.CreateMap(typeof(T), GetType());
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Common.Mappings;
public class MappingProfile : Profile
{
public MappingProfile()
{
ApplyMappingsFromAssembly(Assembly.GetExecutingAssembly());
}
private void ApplyMappingsFromAssembly(Assembly assembly)
{
var types = assembly.GetExportedTypes()
.Where(t => t.GetInterfaces()
.Any(i =>
i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IMapFrom<>)
)
)
.ToList();
foreach (var type in types)
{
var instance = Activator.CreateInstance(type);
var methodInfo =
type.GetMethod("Mapping") ??
type.GetInterface("IMapFrom`1")?.GetMethod("Mapping");
methodInfo?.Invoke(instance, new object[] { this });
}
}
}

View File

@ -0,0 +1,23 @@
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
namespace cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers;
public class DateTimeOffsetToLocalResolver :
IMemberValueResolver<object, object, DateTimeOffset, DateTimeOffset>
{
private readonly TimeZoneService _timeZoneService;
public DateTimeOffsetToLocalResolver(TimeZoneService timeZoneService)
{
_timeZoneService = timeZoneService;
}
public DateTimeOffset Resolve(
object source, object destination,
DateTimeOffset sourceMember, DateTimeOffset destinationMember,
ResolutionContext context)
{
return TimeZoneInfo.ConvertTime(sourceMember, _timeZoneService.TimeZone);
}
}

View File

@ -0,0 +1,21 @@
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application.Common.Models;
public abstract class IdentityRole : Enumeration<IdentityRole>
{
public static readonly IdentityRole Administrator = new AdministratorRole();
public static readonly IdentityRole User = new UserRole();
protected IdentityRole(int value, string name) : base(value, name) { }
private sealed class AdministratorRole : IdentityRole
{
public AdministratorRole() : base(0, "administrator") { }
}
private sealed class UserRole : IdentityRole
{
public UserRole() : base(1, "user") { }
}
}

View File

@ -0,0 +1,23 @@
namespace cuqmbr.TravelGuide.Application.Common.Models;
public class PaginatedList<T>
{
public IReadOnlyCollection<T> Items { get; }
public int PageNumber { get; }
public int TotalPages { get; }
public int TotalCount { get; }
public PaginatedList(
IReadOnlyCollection<T> items, int count,
int pageNumber, int pageSize)
{
PageNumber = pageNumber;
TotalPages = (int) Math.Ceiling(count / (double) pageSize);
TotalCount = count;
Items = items;
}
public bool HasPreviousPage => PageNumber > 1;
public bool HasNextPage => PageNumber < TotalPages;
}

View File

@ -0,0 +1,8 @@
namespace cuqmbr.TravelGuide.Application.Common.ViewModels;
public sealed class PageQuery
{
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
}

View File

@ -0,0 +1,6 @@
namespace cuqmbr.TravelGuide.Application.Common.ViewModels;
public sealed class SearchQuery
{
public string Search { get; set; } = String.Empty;
}

View File

@ -0,0 +1,7 @@
namespace cuqmbr.TravelGuide.Application.Common.ViewModels;
public sealed class SortQuery
{
public string Sort { get; set; } = String.Empty;
}

View File

@ -0,0 +1,28 @@
namespace cuqmbr.TravelGuide.Application;
public sealed class ConfigurationOptions
{
public static string SectionName { get; } = "Application";
public LocalizationConfigurationOptions Localization { get; set; } = new();
public LoggingConfigurationOptions Logging { get; set; } = new();
}
public sealed class LocalizationConfigurationOptions
{
public string DefaultCultureName { get; set; } = "en-US";
public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(30);
}
public sealed class LoggingConfigurationOptions
{
public string Type { get; set; } = "SimpleConsole";
public string LogLevel { get; set; } = "Information";
public string TimestampFormat { get; set; } = "yyyy-MM-ddTHH:mm:ss.fffK";
public bool UseUtcTimestamp { get; set; } = true;
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry;
public record AddCountryCommand : IRequest<CountryDto>
{
public string Name { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry;
public class AddCountryCommandAuthorizer :
AbstractRequestAuthorizer<AddCountryCommand>
{
private readonly SessionUserService _sessionUserService;
public AddCountryCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddCountryCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,49 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry;
public class AddCountryCommandHandler :
IRequestHandler<AddCountryCommand, CountryDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public AddCountryCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<CountryDto> Handle(
AddCountryCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.CountryRepository.GetOneAsync(
e => e.Name == request.Name, cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException(
"Country with given name already exists.");
}
entity = new Country()
{
Name = request.Name
};
entity = await _unitOfWork.CountryRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<CountryDto>(entity);
}
}

View File

@ -0,0 +1,23 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry;
public class AddCountryCommandValidator : AbstractValidator<AddCountryCommand>
{
public AddCountryCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry;
public record DeleteCountryCommand : IRequest
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry;
public class DeleteCountryCommandAuthorizer :
AbstractRequestAuthorizer<DeleteCountryCommand>
{
private readonly SessionUserService _sessionUserService;
public DeleteCountryCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(DeleteCountryCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,34 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry;
public class DeleteCountryCommandHandler : IRequestHandler<DeleteCountryCommand>
{
private readonly UnitOfWork _unitOfWork;
public DeleteCountryCommandHandler(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(
DeleteCountryCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.CountryRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
await _unitOfWork.CountryRepository.DeleteOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry;
public class DeleteCountryCommandValidator : AbstractValidator<DeleteCountryCommand>
{
public DeleteCountryCommandValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,10 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry;
public record UpdateCountryCommand : IRequest<CountryDto>
{
public Guid Guid { get; set; }
public string Name { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry;
public class UpdateCountryCommandAuthorizer :
AbstractRequestAuthorizer<UpdateCountryCommand>
{
private readonly SessionUserService _sessionUserService;
public UpdateCountryCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(UpdateCountryCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,52 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry;
public class UpdateCountryCommandHandler :
IRequestHandler<UpdateCountryCommand, CountryDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public UpdateCountryCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<CountryDto> Handle(
UpdateCountryCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.CountryRepository.GetOneAsync(
e => e.Name == request.Name, cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException();
}
entity = await _unitOfWork.CountryRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
entity.Name = request.Name;
entity = await _unitOfWork.CountryRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<CountryDto>(entity);
}
}

View File

@ -0,0 +1,27 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry;
public class UpdateCountryCommandValidator : AbstractValidator<UpdateCountryCommand>
{
public UpdateCountryCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
}
}

View File

@ -0,0 +1,19 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Countries;
public sealed class CountryDto : IMapFrom<Country>
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Country, CountryDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid));
}
}

View File

@ -0,0 +1,15 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage;
public record GetCountriesPageQuery : IRequest<PaginatedList<CountryDto>>
{
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Search { get; set; } = String.Empty;
public string Sort { get; set; } = String.Empty;
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage;
public class GetCountriesPageQueryAuthorizer :
AbstractRequestAuthorizer<GetCountriesPageQuery>
{
private readonly SessionUserService _sessionUserService;
public GetCountriesPageQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetCountriesPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,44 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage;
public class GetCountriesPageQueryHandler :
IRequestHandler<GetCountriesPageQuery, PaginatedList<CountryDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetCountriesPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<CountryDto>> Handle(
GetCountriesPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.CountryRepository.GetPageAsync(
e => e.Name.ToLower().Contains(request.Search.ToLower()),
request.PageNumber, request.PageSize, cancellationToken);
var mappedItems = _mapper
.ProjectTo<CountryDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<CountryDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<CountryDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,44 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage;
public class GetCountriesPageQueryValidator :
AbstractValidator<GetCountriesPageQuery>
{
public GetCountriesPageQueryValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1));
RuleFor(v => v.PageSize)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1))
.LessThanOrEqualTo(50)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.LessThanOrEqualTo"],
50));
RuleFor(v => v.Search)
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry;
public record GetCountryQuery : IRequest<CountryDto>
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry;
public class GetCountryQueryAuthorizer :
AbstractRequestAuthorizer<GetCountryQuery>
{
private readonly SessionUserService _sessionUserService;
public GetCountryQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetCountryQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,38 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry;
public class GetCountryQueryHandler :
IRequestHandler<GetCountryQuery, CountryDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetCountryQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<CountryDto> Handle(
GetCountryQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.CountryRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
_unitOfWork.Dispose();
if (entity == null)
{
throw new NotFoundException();
}
return _mapper.Map<CountryDto>(entity);
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry;
public class GetCountryQueryValidator : AbstractValidator<GetCountryQuery>
{
public GetCountryQueryValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,10 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion;
public record AddRegionCommand : IRequest<RegionDto>
{
public string Name { get; set; }
public Guid CountryUuid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion;
public class AddRegionCommandAuthorizer :
AbstractRequestAuthorizer<AddRegionCommand>
{
private readonly SessionUserService _sessionUserService;
public AddRegionCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddRegionCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,60 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion;
public class AddRegionCommandHandler :
IRequestHandler<AddRegionCommand, RegionDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public AddRegionCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<RegionDto> Handle(
AddRegionCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.RegionRepository.GetOneAsync(
e => e.Name == request.Name && e.Country.Guid == request.CountryUuid,
cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException(
"Region with given name already exists.");
}
var parentEntity = await _unitOfWork.CountryRepository.GetOneAsync(
e => e.Guid == request.CountryUuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CountryUuid} not found.");
}
entity = new Region()
{
Name = request.Name,
CountryId = parentEntity.Id
};
entity = await _unitOfWork.RegionRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<RegionDto>(entity);
}
}

View File

@ -0,0 +1,27 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion;
public class AddRegionCommandValidator : AbstractValidator<AddRegionCommand>
{
public AddRegionCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.CountryUuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion;
public record DeleteRegionCommand : IRequest
{
public Guid Uuid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion;
public class DeleteRegionCommandAuthorizer :
AbstractRequestAuthorizer<DeleteRegionCommand>
{
private readonly SessionUserService _sessionUserService;
public DeleteRegionCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(DeleteRegionCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,34 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion;
public class DeleteRegionCommandHandler : IRequestHandler<DeleteRegionCommand>
{
private readonly UnitOfWork _unitOfWork;
public DeleteRegionCommandHandler(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(
DeleteRegionCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.RegionRepository.GetOneAsync(
e => e.Guid == request.Uuid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
await _unitOfWork.RegionRepository.DeleteOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion;
public class DeleteRegionCommandValidator : AbstractValidator<DeleteRegionCommand>
{
public DeleteRegionCommandValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Uuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,12 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion;
public record UpdateRegionCommand : IRequest<RegionDto>
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public Guid CountryUuid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion;
public class UpdateRegionCommandAuthorizer :
AbstractRequestAuthorizer<UpdateRegionCommand>
{
private readonly SessionUserService _sessionUserService;
public UpdateRegionCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(UpdateRegionCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,54 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion;
public class UpdateRegionCommandHandler :
IRequestHandler<UpdateRegionCommand, RegionDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public UpdateRegionCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<RegionDto> Handle(
UpdateRegionCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.RegionRepository.GetOneAsync(
e => e.Guid == request.Uuid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
var parentEntity = await _unitOfWork.CountryRepository.GetOneAsync(
e => e.Guid == request.CountryUuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CountryUuid} not found.");
}
entity.Name = request.Name;
entity.CountryId = parentEntity.Id;
entity = await _unitOfWork.RegionRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<RegionDto>(entity);
}
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion;
public class UpdateRegionCommandValidator : AbstractValidator<UpdateRegionCommand>
{
public UpdateRegionCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Uuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.CountryUuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion;
public record GetRegionQuery : IRequest<RegionDto>
{
public Guid Uuid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion;
public class GetRegionQueryAuthorizer :
AbstractRequestAuthorizer<GetRegionQuery>
{
private readonly SessionUserService _sessionUserService;
public GetRegionQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetRegionQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,39 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion;
public class GetRegionQueryHandler :
IRequestHandler<GetRegionQuery, RegionDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetRegionQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<RegionDto> Handle(
GetRegionQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.RegionRepository.GetOneAsync(
e => e.Guid == request.Uuid, e => e.Country,
cancellationToken);
_unitOfWork.Dispose();
if (entity == null)
{
throw new NotFoundException();
}
return _mapper.Map<RegionDto>(entity);
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion;
public class GetRegionQueryValidator : AbstractValidator<GetRegionQuery>
{
public GetRegionQueryValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Uuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,17 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage;
public record GetRegionsPageQuery : IRequest<PaginatedList<RegionDto>>
{
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Search { get; set; } = String.Empty;
public string Sort { get; set; } = String.Empty;
public Guid? CountryUuid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage;
public class GetRegionsPageQueryAuthorizer :
AbstractRequestAuthorizer<GetRegionsPageQuery>
{
private readonly SessionUserService _sessionUserService;
public GetRegionsPageQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetRegionsPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,51 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage;
public class GetRegionsPageQueryHandler :
IRequestHandler<GetRegionsPageQuery, PaginatedList<RegionDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetRegionsPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<RegionDto>> Handle(
GetRegionsPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.RegionRepository.GetPageAsync(
e =>
(e.Name.ToLower().Contains(request.Search.ToLower()) ||
e.Country.Name.ToLower().Contains(request.Search.ToLower())) &&
(request.CountryUuid != null
? e.Country.Guid == request.CountryUuid
: true),
e => e.Country,
request.PageNumber, request.PageSize,
cancellationToken);
var mappedItems = _mapper
.ProjectTo<RegionDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<RegionDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<RegionDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,43 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage;
public class GetRegionsPageQueryValidator : AbstractValidator<GetRegionsPageQuery>
{
public GetRegionsPageQueryValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1));
RuleFor(v => v.PageSize)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1))
.LessThanOrEqualTo(50)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.LessThanOrEqualTo"],
50));
RuleFor(v => v.Search)
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
}
}

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