Init
@@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
||||
@@ -0,0 +1,363 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# 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/
|
||||
[Oo]ut/
|
||||
[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 Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# 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
|
||||
*.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 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/
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" 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.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BookNet.Application\BookNet.Application.csproj" />
|
||||
<ProjectReference Include="..\BookNet.Infrastructure\BookNet.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,120 @@
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Dto.Auth;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace BookNet.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
public AuthController(IAuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
}
|
||||
|
||||
[HttpPost("signup")]
|
||||
public async Task<ActionResult<MessageDto>> SignUp([FromBody] SignUpDto request)
|
||||
{
|
||||
var result = await _authService.SignUpAsync(request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
|
||||
return CreatedAtAction(nameof(SignUp), new MessageDto(result.Value, 200));
|
||||
}
|
||||
|
||||
[HttpPost("signin")]
|
||||
public async Task<ActionResult<AuthDto>> SignIn([FromBody] SignInDto request)
|
||||
{
|
||||
var result = await _authService.SignInAsync(request, GetFingerprintString());
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
|
||||
AppendCookie(result.Value.RefreshToken, 604800);
|
||||
return CreatedAtAction(nameof(SignUp), result.Value);
|
||||
}
|
||||
|
||||
[HttpPost("refresh")]
|
||||
public async Task<ActionResult<AuthDto>> Refresh([FromBody] RefreshDto? refresh)
|
||||
{
|
||||
var refreshToken = "";
|
||||
if (!string.IsNullOrEmpty(refresh?.RefreshToken))
|
||||
refreshToken = refresh.RefreshToken;
|
||||
else
|
||||
Request.Cookies.TryGetValue("refreshToken", out refreshToken);
|
||||
if (string.IsNullOrEmpty(refreshToken))
|
||||
return Unauthorized();
|
||||
var result = await _authService.RefreshTokenAsync(refreshToken, GetFingerprintString());
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
|
||||
AppendCookie(result.Value.RefreshToken, 604800);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("logout")]
|
||||
public async Task<ActionResult<MessageDto>> Logout()
|
||||
{
|
||||
var userId = Request.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Unauthorized();
|
||||
var result = await _authService.LogoutAsync(Guid.Parse(userId.Value));
|
||||
if (!result.IsSuccess)
|
||||
return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
Response.Cookies.Delete("refreshToken");
|
||||
return Ok(new MessageDto(result.Value ?? "Success", 200));
|
||||
}
|
||||
|
||||
[HttpPost("send-verification-code")]
|
||||
public async Task<ActionResult<MessageDto>> SendVerificationCode([FromBody] SendCodeDto request)
|
||||
{
|
||||
var result = await _authService.SendVerificationCodeAsync(request.Email);
|
||||
if (!result.IsSuccess) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(new MessageDto(result.Value ?? "Success", 200));
|
||||
}
|
||||
|
||||
[HttpPost("verify-code")]
|
||||
public async Task<ActionResult<MessageDto>> VerifyCode([FromBody] VerifyCodeDto request)
|
||||
{
|
||||
var result = await _authService.VerifyCodeAsync(request.Email, request.Code);
|
||||
if (!result.IsSuccess) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(new MessageDto(result.Value ?? "Success", 200));
|
||||
}
|
||||
|
||||
[HttpPost("verify-account")]
|
||||
public async Task<ActionResult<MessageDto>> VerifyAccount([FromBody] VerifyAccountDto request)
|
||||
{
|
||||
var result = await _authService.VerifyAccountAsync(request.Email, request.Code);
|
||||
if (!result.IsSuccess) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(new MessageDto(result.Value ?? "Success", 200));
|
||||
}
|
||||
|
||||
[HttpPost("reset-password")]
|
||||
public async Task<ActionResult<MessageDto>> ResetPassword([FromBody] ResetPasswordDto request)
|
||||
{
|
||||
var result = await _authService.ResetPasswordAsync(request.Email, request.Code, request.NewPassword);
|
||||
if (!result.IsSuccess) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(new MessageDto(result.Value ?? "Success", 200));
|
||||
}
|
||||
|
||||
private void AppendCookie(string refreshToken, int maxAge)
|
||||
{
|
||||
Response.Cookies.Append("refreshToken", refreshToken, new CookieOptions()
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = Request.IsHttps ? true : false,
|
||||
SameSite = SameSiteMode.Lax,
|
||||
MaxAge = TimeSpan.FromSeconds(maxAge)
|
||||
});
|
||||
}
|
||||
|
||||
private string GetFingerprintString()
|
||||
{
|
||||
IHeaderDictionary headers = Request.Headers;
|
||||
return $"{headers.UserAgent};{headers.AcceptLanguage};{headers.AcceptEncoding};";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Dto.Authors;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BookNet.API.Controllers
|
||||
{
|
||||
[Authorize(Roles = "Admin")]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AuthorsController : ControllerBase
|
||||
{
|
||||
private readonly IAuthorService _authorService;
|
||||
|
||||
public AuthorsController(IAuthorService authorService)
|
||||
{
|
||||
_authorService = authorService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Author>> AddAuthor([FromBody] AuthorDto request)
|
||||
{
|
||||
var result = await _authorService.AddAsync(request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return CreatedAtAction(nameof(AddAuthor), result.Value);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<Author>> EditAuthor(Guid id, [FromBody] AuthorDto request)
|
||||
{
|
||||
var result = await _authorService.UpdateAsync(id, request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult<MessageDto>> DeleteAuthor(Guid id)
|
||||
{
|
||||
var result = await _authorService.DeleteAsync(id);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(new MessageDto(result.Value, 200));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<GetAuthorDto>> GetAllAuthors([FromQuery] GetAuthorParamsDto request)
|
||||
{
|
||||
var result = await _authorService.GetAsync(request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<Author>> GetAuthorById(Guid id)
|
||||
{
|
||||
var result = await _authorService.GetByIdAsync(id);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using Azure.Core;
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Dto.Books;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.IO;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace BookNet.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class BooksController : ControllerBase
|
||||
{
|
||||
private readonly IBookService _bookService;
|
||||
public BooksController(IBookService bookService)
|
||||
{
|
||||
_bookService = bookService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<GetBookDto>> GetAllBooks([FromQuery] GetBookParamsDto request)
|
||||
{
|
||||
var result = await _bookService.GetAsync(request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<BookDto>> GetBook(Guid id)
|
||||
{
|
||||
var userId = Request.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
if (User.IsInRole("Admin"))
|
||||
{
|
||||
var adminResult = await _bookService.GetByIdAsync(id);
|
||||
if (!adminResult.IsSuccess || adminResult.Value == null)
|
||||
return StatusCode(adminResult.Status, new ErrorDto(adminResult.Errors ?? [], adminResult.Status));
|
||||
return Ok(adminResult.Value);
|
||||
}
|
||||
|
||||
var result = await _bookService.GetByIdUserAsync(id, Guid.Parse(userId.Value));
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Book>> AddBook([FromForm] AddBookDto request)
|
||||
{
|
||||
var result = await _bookService.AddAsync(request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return CreatedAtAction(nameof(AddBook), result.Value);
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<Book>> EditBook(Guid id, [FromForm] AddBookDto request)
|
||||
{
|
||||
var result = await _bookService.UpdateAsync(id, request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult<MessageDto>> DeleteBook(Guid id)
|
||||
{
|
||||
var result = await _bookService.DeleteAsync(id);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(new MessageDto(result.Value, 200));
|
||||
}
|
||||
|
||||
[Authorize(Roles = "User")]
|
||||
[HttpGet("library")]
|
||||
public async Task<ActionResult<GetBookDto>> GetUserBooks([FromQuery] GetBookParamsDto request)
|
||||
{
|
||||
var userId = Request.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var result = await _bookService.GetUserLibraryAsync(Guid.Parse(userId.Value), request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[Authorize(Roles = "User")]
|
||||
[HttpGet("wishlist")]
|
||||
public async Task<ActionResult<GetBookDto>> GetMyWishlist([FromQuery] GetBookParamsDto request)
|
||||
{
|
||||
var userId = Request.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var result = await _bookService.GetUserWishlistAsync(Guid.Parse(userId.Value), request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[Authorize(Roles = "User")]
|
||||
[HttpPost("wishlist/{id}")]
|
||||
public async Task<ActionResult<MessageDto>> WishlistBook(Guid id)
|
||||
{
|
||||
var userId = Request.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var result = await _bookService.ToggleWishlist(Guid.Parse(userId.Value), id);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(new MessageDto(result.Value, 200));
|
||||
}
|
||||
|
||||
[Authorize(Roles = "User")]
|
||||
[HttpPost("buy/{id}")]
|
||||
public async Task<ActionResult<MessageDto>> BuyBook(Guid id)
|
||||
{
|
||||
var userId = Request.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var result = await _bookService.BuyBook(Guid.Parse(userId.Value), id);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(new MessageDto(result.Value, 200));
|
||||
}
|
||||
|
||||
[Authorize(Roles = "User")]
|
||||
[HttpGet("{id}/file")]
|
||||
public async Task<ActionResult> DownloadBook(Guid id)
|
||||
{
|
||||
var userId = Request.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var result = await _bookService.DownloadBook(Guid.Parse(userId.Value), id);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
|
||||
var contentType = "application/epub+zip";
|
||||
return PhysicalFile(result.Value, contentType, fileDownloadName: $"{id}.epub");
|
||||
}
|
||||
|
||||
[Authorize(Roles = "User")]
|
||||
[HttpPost("review/{id}")]
|
||||
public async Task<ActionResult<ReviewDto>> ReviewBook(Guid id, [FromBody] WriteReviewDto review)
|
||||
{
|
||||
var userId = Request.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var result = await _bookService.AddReview(Guid.Parse(userId.Value), id, review);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return CreatedAtAction(nameof(AddBook), result.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Dto.Genres;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BookNet.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class GenresController : ControllerBase
|
||||
{
|
||||
private readonly IGenreService _genreService;
|
||||
public GenresController(IGenreService genreService)
|
||||
{
|
||||
_genreService = genreService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<GetGenreDto>> GetAllGenres([FromQuery] GetGenreParamsDto request)
|
||||
{
|
||||
var result = await _genreService.GetAsync(request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<GenreDto>> GetGenreById(Guid id)
|
||||
{
|
||||
var result = await _genreService.GetByIdAsync(id);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<GenreDto>> AddGenre([FromForm] AddGenreDto request)
|
||||
{
|
||||
var result = await _genreService.AddAsync(request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return CreatedAtAction(nameof(AddGenre), result.Value);
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<GenreDto>> EditGenre(Guid id, [FromForm] AddGenreDto request)
|
||||
{
|
||||
var result = await _genreService.UpdateAsync(id, request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult<MessageDto>> DeleteGenre(Guid id)
|
||||
{
|
||||
var result = await _genreService.DeleteAsync(id);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(new MessageDto(result.Value, 200));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Dto.Publishers;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BookNet.API.Controllers
|
||||
{
|
||||
[Authorize(Roles = "Admin")]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class PublishersController : ControllerBase
|
||||
{
|
||||
private readonly IPublisherService _publisherService;
|
||||
|
||||
public PublishersController(IPublisherService publisherService)
|
||||
{
|
||||
_publisherService = publisherService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Publisher>> AddPublisher([FromBody] AddPublisherDto request)
|
||||
{
|
||||
var result = await _publisherService.AddAsync(request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return CreatedAtAction(nameof(AddPublisher), result.Value);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<Publisher>> EditPublisher(Guid id, [FromBody] AddPublisherDto request)
|
||||
{
|
||||
var result = await _publisherService.UpdateAsync(id, request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult<MessageDto>> DeletePublisher(Guid id)
|
||||
{
|
||||
var result = await _publisherService.DeleteAsync(id);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(new MessageDto(result.Value, 200));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<GetPublisherDto>> GetAllPublishers([FromQuery] GetPublisherParamsDto request)
|
||||
{
|
||||
var result = await _publisherService.GetAsync(request);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<Publisher>> GetPublisherById(Guid id)
|
||||
{
|
||||
var result = await _publisherService.GetByIdAsync(id);
|
||||
if (!result.IsSuccess || result.Value == null) return StatusCode(result.Status, new ErrorDto(result.Errors ?? [], result.Status));
|
||||
return Ok(result.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using AutoMapper;
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace BookNet.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
public UsersController(IUserService userService)
|
||||
{
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<UserDto>> Profile()
|
||||
{
|
||||
var userId = User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Unauthorized();
|
||||
var user = await _userService.GetUserByIdAsync(Guid.Parse(userId.Value));
|
||||
if (!user.IsSuccess || user.Value == null) return StatusCode(user.Status, new ErrorDto(user.Errors ?? [], user.Status));
|
||||
return Ok(user.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
using Booknet.Application.Common;
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Interfaces.Repository;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Application.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
using BookNet.Infrastructure.Data;
|
||||
using BookNet.Infrastructure.Data.Repository;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
//DbContext
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
//Repository
|
||||
builder.Services.AddScoped<IAuthorRepository, AuthorRepository>();
|
||||
builder.Services.AddScoped<IBookRepository, BookRepository>();
|
||||
builder.Services.AddScoped<IGenreRepository, GenreRepository>();
|
||||
builder.Services.AddScoped<IPublisherRepository, PublisherRepository>();
|
||||
builder.Services.AddScoped<IRefreshRepository, RefreshRepository>();
|
||||
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
builder.Services.AddScoped<IEmailRepository, EmailRepository>();
|
||||
builder.Services.AddScoped<IReviewRepository, ReviewRepository>();
|
||||
|
||||
//Services
|
||||
builder.Services.AddSingleton(sp =>
|
||||
{
|
||||
var env = sp.GetRequiredService<IWebHostEnvironment>();
|
||||
return new WebRootOptions { WebRootPath = env.WebRootPath };
|
||||
});
|
||||
builder.Services.AddScoped<IEmailService, ConsoleEmailService>();
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddScoped<IRefreshService, RefreshService>();
|
||||
builder.Services.AddScoped<IJwtService, JwtService>();
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
builder.Services.AddScoped<ICryptoService, CryptoService>();
|
||||
builder.Services.AddScoped<IAuthorService, AuthorService>();
|
||||
builder.Services.AddScoped<IGenreService, GenreService>();
|
||||
builder.Services.AddScoped<IPublisherService, PublisherService>();
|
||||
builder.Services.AddScoped<IBookService, BookService>();
|
||||
|
||||
//AutoMapper
|
||||
builder.Services.AddAutoMapper(cfg => cfg.AddProfile<MappingProfile>());
|
||||
|
||||
//JWT
|
||||
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("JwtSettings"));
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
||||
builder.Configuration["JwtSettings:SecretKey"]!)),
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = builder.Configuration["JwtSettings:Issuer"],
|
||||
ValidateAudience = true,
|
||||
ValidAudience = builder.Configuration["JwtSettings:Audience"]
|
||||
};
|
||||
});
|
||||
|
||||
//Identity
|
||||
builder.Services
|
||||
.AddIdentityCore<User>(options =>
|
||||
{
|
||||
options.User.RequireUniqueEmail = true;
|
||||
options.SignIn.RequireConfirmedEmail = true;
|
||||
})
|
||||
.AddRoles<IdentityRole<Guid>>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddSignInManager()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
//Rate limiting
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
|
||||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
|
||||
{
|
||||
return RateLimitPartition.GetFixedWindowLimiter(
|
||||
partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
factory: _ => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = 30,
|
||||
Window = TimeSpan.FromMinutes(1),
|
||||
QueueLimit = 0,
|
||||
AutoReplenishment = true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//CORS
|
||||
var corsOrigins = builder.Configuration["AllowedCorsOrigins"]?.Split(';', StringSplitOptions.RemoveEmptyEntries) ?? [];
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowSPA", policy =>
|
||||
policy.WithOrigins(corsOrigins)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials());
|
||||
});
|
||||
|
||||
// Controllers
|
||||
builder.Services.AddControllers()
|
||||
.ConfigureApiBehaviorOptions(options =>
|
||||
{
|
||||
options.InvalidModelStateResponseFactory = context =>
|
||||
{
|
||||
var messages = context.ModelState
|
||||
.SelectMany(x => x.Value.Errors)
|
||||
.Select(x => x.ErrorMessage)
|
||||
.ToArray();
|
||||
var response = new { status = 400, errors = messages };
|
||||
return new BadRequestObjectResult(response);
|
||||
};
|
||||
})
|
||||
.AddJsonOptions(x =>
|
||||
x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);
|
||||
|
||||
// Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
//Migrations
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
context.Database.Migrate();
|
||||
}
|
||||
|
||||
//CORS
|
||||
app.UseCors("AllowSPA");
|
||||
app.UseCookiePolicy(new CookiePolicyOptions
|
||||
{
|
||||
MinimumSameSitePolicy = SameSiteMode.None,
|
||||
HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
|
||||
Secure = CookieSecurePolicy.Always,
|
||||
});
|
||||
|
||||
// Configure pipeline
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseRateLimiter();
|
||||
app.UseStaticFiles();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://192.168.1.5:5000"
|
||||
},
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://192.168.1.5:5000"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Username=postgres;Database=booknet;Password=StrongPassword"
|
||||
},
|
||||
"JwtSettings": {
|
||||
"SecretKey": "<YourVeryVeryVeryVerySecretJWTKey>",
|
||||
"Issuer": "PassManAPI",
|
||||
"Audience": "PassManApplication",
|
||||
"AccessTokenLifetimeMinutes": 15
|
||||
},
|
||||
"AllowedCorsOrigins": "http://localhost:3000",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 408 KiB |
|
After Width: | Height: | Size: 408 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.9" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BookNet.Domain\BookNet.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace BookNet.Application.Common
|
||||
{
|
||||
public class JwtSettings
|
||||
{
|
||||
public string SecretKey { get; set; } = string.Empty;
|
||||
public string Issuer { get; set; } = string.Empty;
|
||||
public string Audience { get; set; } = string.Empty;
|
||||
public int AccessTokenLifetimeMinutes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using AutoMapper;
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Dto.Books;
|
||||
using BookNet.Application.Dto.Genres;
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace Booknet.Application.Common
|
||||
{
|
||||
public class MappingProfile : Profile
|
||||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
CreateMap<User, UserDto>().ReverseMap();
|
||||
CreateMap<Review, ReviewDto>();
|
||||
CreateMap<Genre, GenreDto>();
|
||||
CreateMap<Book, BookDto>()
|
||||
.ForMember(dest => dest.Genres, opt => opt.MapFrom(src => src.Genres))
|
||||
.ForMember(dest => dest.Reviews, opt => opt.MapFrom(src => src.Reviews));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace BookNet.Application.Common
|
||||
{
|
||||
public class Result<T>
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public T? Value { get; set; }
|
||||
public string[]? Errors { get; set; }
|
||||
public int Status { get; set; }
|
||||
|
||||
public static Result<T> Success(T value) =>
|
||||
new() { IsSuccess = true, Value = value, Status = 200 };
|
||||
|
||||
public static Result<T> Failure(int status = 400, params string[]? errors) =>
|
||||
new() { IsSuccess = false, Errors = errors, Status = status };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace BookNet.Application.Common
|
||||
{
|
||||
public sealed class WebRootOptions
|
||||
{
|
||||
public required string WebRootPath { get; init; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace BookNet.Application.Dto.Auth
|
||||
{
|
||||
public class AuthDto
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace BookNet.Application.Dto.Auth
|
||||
{
|
||||
public class RefreshDto
|
||||
{
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Auth
|
||||
{
|
||||
public class ResetPasswordDto
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public required string Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[MinLength(4)]
|
||||
[MaxLength(4)]
|
||||
public required string Code { get; set; }
|
||||
|
||||
[Required]
|
||||
[MinLength(8)]
|
||||
[MaxLength(100)]
|
||||
[RegularExpression(@"^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^\w\s]).{8,100}$")]
|
||||
public required string NewPassword { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Auth
|
||||
{
|
||||
public class SendCodeDto
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Auth
|
||||
{
|
||||
public class SignInDto
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[MinLength(2)]
|
||||
[MaxLength(100)]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MinLength(8)]
|
||||
[MaxLength(100)]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Auth
|
||||
{
|
||||
public class SignUpDto
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[MinLength(2)]
|
||||
[MaxLength(100)]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MinLength(8)]
|
||||
[MaxLength(100)]
|
||||
[RegularExpression(@"^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^\w\s]).{8,100}$")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
[MaxLength(100)]
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
[MaxLength(100)]
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Auth
|
||||
{
|
||||
public class VerifyAccountDto
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MinLength(4)]
|
||||
[MaxLength(4)]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Auth
|
||||
{
|
||||
public class VerifyCodeDto
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MinLength(4)]
|
||||
[MaxLength(4)]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Authors
|
||||
{
|
||||
public class AuthorDto
|
||||
{
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
[MaxLength(100)]
|
||||
public required string FirstName { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
[MaxLength(100)]
|
||||
public required string LastName { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Dto.Authors
|
||||
{
|
||||
public class GetAuthorDto
|
||||
{
|
||||
public ICollection<Author> Authors { get; set; } = new List<Author>();
|
||||
public int TotalCount { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Authors
|
||||
{
|
||||
public class GetAuthorParamsDto
|
||||
{
|
||||
[Range(1, 1000, ErrorMessage = "Страница должна быть от 1 до 1000")]
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
[Range(1, 100, ErrorMessage = "Размер страницы должен быть от 1 до 100")]
|
||||
public int PageSize { get; set; } = 10;
|
||||
|
||||
[MaxLength(100, ErrorMessage = "Строка поиска: максимум 100 символов")]
|
||||
public string Search { get; set; } = string.Empty;
|
||||
|
||||
[RegularExpression(@"^(asc|desc)$",
|
||||
ErrorMessage = "Недопустимое поле сортировки: asc, desc")]
|
||||
public string Sort { get; set; } = "asc";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Books
|
||||
{
|
||||
public class AddBookDto
|
||||
{
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
[MaxLength(100)]
|
||||
public required string Title { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string Description { get; set; }
|
||||
|
||||
[Required]
|
||||
public required Guid AuthorId { get; set; }
|
||||
|
||||
[Required]
|
||||
public required Guid PublisherId { get; set; }
|
||||
|
||||
[Required]
|
||||
public required decimal Price { get; set; }
|
||||
|
||||
[Required]
|
||||
public required int Pages { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string ISBN { get; set; }
|
||||
|
||||
[Required]
|
||||
public required DateOnly ReleaseDate { get; set; }
|
||||
|
||||
[Required]
|
||||
public required IFormFile CoverFile { get; set; }
|
||||
|
||||
[Required]
|
||||
public required IFormFile BookFile { get; set; }
|
||||
|
||||
[Required]
|
||||
public required Guid[] GenreIds { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using BookNet.Application.Dto.Genres;
|
||||
using BookNet.Domain.Entities;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BookNet.Application.Dto.Books
|
||||
{
|
||||
public class BookDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string ISBN { get; set; } = string.Empty;
|
||||
public required Author Author { get; set; }
|
||||
public required Publisher Publisher { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
public int Pages { get; set; }
|
||||
public required string Size { get; set; }
|
||||
public DateOnly ReleaseDate { get; set; }
|
||||
public string FormatDate => ReleaseDate.ToString("d MMM, yyyy", CultureInfo.GetCultureInfo("ru"));
|
||||
public string CoverUrl { get => $"/books/{Id}/cover.jpg"; }
|
||||
|
||||
public bool IsPurchased { get; set; }
|
||||
public bool IsInWishlist { get; set; }
|
||||
public double Rating
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Reviews != null && Reviews.Count == 0)
|
||||
return 0;
|
||||
|
||||
return Math.Round(Reviews.Average(r => r.Stars), 1);
|
||||
}
|
||||
}
|
||||
public int Purchases { get; set; }
|
||||
public ICollection<ReviewDto> Reviews { get; set; } = new List<ReviewDto>();
|
||||
public ICollection<GenreDto> Genres { get; set; } = new List<GenreDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BookNet.Application.Dto.Books
|
||||
{
|
||||
public class EditBookDto
|
||||
{
|
||||
public required string Title { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public required Guid AuthorId { get; set; }
|
||||
public required Guid PublisherId { get; set; }
|
||||
public required decimal Price { get; set; }
|
||||
public required int Pages { get; set; }
|
||||
public required string ISBN { get; set; }
|
||||
public required DateTime ReleaseDate { get; set; }
|
||||
public IFormFile? CoverFile { get; set; } = null;
|
||||
public IFormFile? BookFile { get; set; } = null;
|
||||
public required List<Guid> GenreIds { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace BookNet.Application.Dto.Books
|
||||
{
|
||||
public class GetBookDto
|
||||
{
|
||||
public ICollection<BookDto> Books { get; set; } = new List<BookDto>();
|
||||
public int TotalCount { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Books
|
||||
{
|
||||
public class GetBookParamsDto
|
||||
{
|
||||
[Range(1, 1000, ErrorMessage = "Страница должна быть от 1 до 1000")]
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
[Range(1, 100, ErrorMessage = "Размер страницы должен быть от 1 до 100")]
|
||||
public int PageSize { get; set; } = 10;
|
||||
|
||||
[MaxLength(100, ErrorMessage = "Строка поиска: максимум 100 символов")]
|
||||
public string? Search { get; set; } = string.Empty;
|
||||
|
||||
[RegularExpression(@"^(price|priceDesc|rating|ratingDesc|releaseDesc)$",
|
||||
ErrorMessage = "Недопустимое поле сортировки: price, priceDesc, rating, ratingDesc, releaseDesc")]
|
||||
public string? Sort { get; set; } = string.Empty;
|
||||
|
||||
[RegularExpression(@"^(four|fourHalf)$")]
|
||||
public string? Rating { get; set; } = string.Empty;
|
||||
|
||||
public Guid[]? Genres { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace BookNet.Application.Dto
|
||||
{
|
||||
public class ErrorDto
|
||||
{
|
||||
public string[] Errors { get; set; }
|
||||
public int Status { get; set; }
|
||||
|
||||
public ErrorDto(string[] errors, int status)
|
||||
{
|
||||
Errors = errors; Status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Genres
|
||||
{
|
||||
public class AddGenreDto
|
||||
{
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
[MaxLength(50)]
|
||||
public required string Name { get; set; }
|
||||
public IFormFile? ImageFile { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace BookNet.Application.Dto.Genres
|
||||
{
|
||||
public class GenreDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string Image { get => $"/genres/{Id}.jpg"; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace BookNet.Application.Dto.Genres
|
||||
{
|
||||
public class GetGenreDto
|
||||
{
|
||||
public ICollection<GenreDto> Genres { get; set; } = new List<GenreDto>();
|
||||
public int TotalCount { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Genres
|
||||
{
|
||||
public class GetGenreParamsDto
|
||||
{
|
||||
[Range(1, 1000, ErrorMessage = "Страница должна быть от 1 до 1000")]
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
[Range(1, 100, ErrorMessage = "Размер страницы должен быть от 1 до 100")]
|
||||
public int PageSize { get; set; } = 10;
|
||||
|
||||
[MaxLength(100, ErrorMessage = "Строка поиска: максимум 100 символов")]
|
||||
public string Search { get; set; } = string.Empty;
|
||||
|
||||
[RegularExpression(@"^(asc|desc)$",
|
||||
ErrorMessage = "Недопустимое поле сортировки: asc, desc")]
|
||||
public string Sort { get; set; } = "asc";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace BookNet.Application.Dto
|
||||
{
|
||||
public class MessageDto
|
||||
{
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public int Status { get; set; }
|
||||
|
||||
public MessageDto(string message, int status)
|
||||
{
|
||||
Message = message;
|
||||
Status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Publishers
|
||||
{
|
||||
public class AddPublisherDto
|
||||
{
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
[MaxLength(100)]
|
||||
public required string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Dto.Publishers
|
||||
{
|
||||
public class GetPublisherDto
|
||||
{
|
||||
public ICollection<Publisher> Publishers { get; set; } = new List<Publisher>();
|
||||
public int TotalCount { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto.Publishers
|
||||
{
|
||||
public class GetPublisherParamsDto
|
||||
{
|
||||
[Range(1, 1000, ErrorMessage = "Страница должна быть от 1 до 1000")]
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
[Range(1, 100, ErrorMessage = "Размер страницы должен быть от 1 до 100")]
|
||||
public int PageSize { get; set; } = 10;
|
||||
|
||||
[MaxLength(100, ErrorMessage = "Строка поиска: максимум 100 символов")]
|
||||
public string Search { get; set; } = string.Empty;
|
||||
|
||||
[RegularExpression(@"^(asc|desc)$",
|
||||
ErrorMessage = "Недопустимое поле сортировки: asc, desc")]
|
||||
public string Sort { get; set; } = "asc";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace BookNet.Application.Dto
|
||||
{
|
||||
public class ReviewDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public required UserDto Author { get; set; }
|
||||
public required string Text { get; set; }
|
||||
public int Stars { get; set; }
|
||||
public required DateTime DateAdded { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace BookNet.Application.Dto
|
||||
{
|
||||
public class UserDto
|
||||
{
|
||||
public required string FirstName { get; set; }
|
||||
public required string LastName { get; set; }
|
||||
public required string Email { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BookNet.Application.Dto
|
||||
{
|
||||
public class WriteReviewDto
|
||||
{
|
||||
[MaxLength(500)]
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[Range(1, 5)]
|
||||
public int Stars { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Repository
|
||||
{
|
||||
public interface IAuthorRepository
|
||||
{
|
||||
Task<ICollection<Author>> GetAsync();
|
||||
Task<Author?> GetByIdAsync(Guid id);
|
||||
Task AddAsync(Author author);
|
||||
Task DeleteAsync(Author author);
|
||||
Task<bool> Exists(Author author);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Repository
|
||||
{
|
||||
public interface IBookRepository
|
||||
{
|
||||
Task<ICollection<Book>> GetAsync();
|
||||
Task<ICollection<Book?>> GetUserLibraryAsync(Guid userId);
|
||||
Task<ICollection<Book?>> GetUserWishlistAsync(Guid userId);
|
||||
Task<Book?> GetByIdAsync(Guid id);
|
||||
Task AddAsync(Book book);
|
||||
Task DeleteAsync(Book book);
|
||||
Task<bool> ExistsAsync(Book book);
|
||||
Task<bool> BookInWishlistAsync(Guid userId, Book book);
|
||||
Task<bool> HasBookAsync(Guid userId, Book book);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Repository
|
||||
{
|
||||
public interface IEmailRepository
|
||||
{
|
||||
Task AddAsync(EmailCode emailCode);
|
||||
Task<EmailCode?> GetAsync(Guid userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Repository
|
||||
{
|
||||
public interface IGenreRepository
|
||||
{
|
||||
Task<ICollection<Genre>> GetAsync();
|
||||
Task<ICollection<Genre>> GetByIdsAsync(Guid[] ids);
|
||||
Task<Genre?> GetByIdAsync(Guid id);
|
||||
Task AddAsync(Genre genre);
|
||||
Task DeleteAsync(Genre genre);
|
||||
Task<bool> Exists(Genre genre);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Repository
|
||||
{
|
||||
public interface IPublisherRepository
|
||||
{
|
||||
Task<ICollection<Publisher>> GetAsync();
|
||||
Task<Publisher?> GetByIdAsync(Guid id);
|
||||
Task AddAsync(Publisher publisher);
|
||||
Task DeleteAsync(Publisher publisher);
|
||||
Task<bool> Exists(Publisher publisher);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Repository
|
||||
{
|
||||
public interface IRefreshRepository
|
||||
{
|
||||
Task AddAsync(RefreshToken refreshToken);
|
||||
Task<RefreshToken?> GetByHashAsync(string hash);
|
||||
Task DeleteAsync(RefreshToken refreshToken);
|
||||
Task DeleteUserTokensAsync(Guid userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using BookNet.Domain.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Repository
|
||||
{
|
||||
public interface IReviewRepository
|
||||
{
|
||||
Task AddAsync(Review review);
|
||||
Task DeleteAsync(Review review);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
namespace BookNet.Application.Interfaces.Repository
|
||||
{
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
IBookRepository Books { get; }
|
||||
IAuthorRepository Authors { get; }
|
||||
IGenreRepository Genres { get; }
|
||||
IPublisherRepository Publishers { get; }
|
||||
IReviewRepository Reviews { get; }
|
||||
IRefreshRepository RefreshTokens { get; }
|
||||
IEmailRepository EmailCodes { get; }
|
||||
Task<int> SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto.Auth;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Services
|
||||
{
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<Result<string>> SignUpAsync(SignUpDto request);
|
||||
Task<Result<AuthDto>> SignInAsync(SignInDto request, string fingerprint);
|
||||
Task<Result<AuthDto>> RefreshTokenAsync(string refreshToken, string fingerprint);
|
||||
Task<Result<string>> LogoutAsync(Guid userId);
|
||||
Task<Result<string>> SendVerificationCodeAsync(string email);
|
||||
Task<Result<string>> VerifyCodeAsync(string email, string code);
|
||||
Task<Result<string>> VerifyAccountAsync(string email, string code);
|
||||
Task<Result<string>> ResetPasswordAsync(string email, string code, string newPassword);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto.Authors;
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Services
|
||||
{
|
||||
public interface IAuthorService
|
||||
{
|
||||
Task<Result<Author>> AddAsync(AuthorDto author);
|
||||
Task<Result<Author>> UpdateAsync(Guid id, AuthorDto author);
|
||||
Task<Result<string>> DeleteAsync(Guid id);
|
||||
Task<Result<GetAuthorDto>> GetAsync(GetAuthorParamsDto authorParams);
|
||||
Task<Result<Author>> GetByIdAsync(Guid id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Dto.Books;
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Services
|
||||
{
|
||||
public interface IBookService
|
||||
{
|
||||
Task<Result<Book>> AddAsync(AddBookDto book);
|
||||
Task<Result<Book>> UpdateAsync(Guid id, AddBookDto book);
|
||||
Task<Result<string>> DeleteAsync(Guid id);
|
||||
Task<Result<GetBookDto>> GetAsync(GetBookParamsDto bookParams);
|
||||
Task<Result<GetBookDto>> GetUserLibraryAsync(Guid userId, GetBookParamsDto bookParams);
|
||||
Task<Result<GetBookDto>> GetUserWishlistAsync(Guid userId, GetBookParamsDto bookParams);
|
||||
Task<Result<Book>> GetByIdAsync(Guid id);
|
||||
Task<Result<BookDto>> GetByIdUserAsync(Guid id, Guid userId);
|
||||
Task<Result<string>> ToggleWishlist(Guid userId, Guid bookId);
|
||||
Task<Result<string>> BuyBook(Guid userId, Guid bookId);
|
||||
Task<Result<string>> DownloadBook(Guid userId, Guid bookId);
|
||||
Task<Result<ReviewDto>> AddReview(Guid userId, Guid bookId, WriteReviewDto review);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace BookNet.Application.Interfaces.Services
|
||||
{
|
||||
public interface ICryptoService
|
||||
{
|
||||
byte[] GenerateSalt();
|
||||
|
||||
string Hash(string text, byte[] salt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Services
|
||||
{
|
||||
public interface IEmailService
|
||||
{
|
||||
string GenerateVerificationCode();
|
||||
|
||||
Task<bool> SendEmailCodeAsync(User user, string code);
|
||||
|
||||
Task<bool> CompareEmailCodeAsync(User user, string code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto.Genres;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Services
|
||||
{
|
||||
public interface IGenreService
|
||||
{
|
||||
Task<Result<GenreDto>> AddAsync(AddGenreDto genreDto);
|
||||
Task<Result<GenreDto>> UpdateAsync(Guid id, AddGenreDto genreDto);
|
||||
Task<Result<GetGenreDto>> GetAsync(GetGenreParamsDto genreParams);
|
||||
Task<Result<GenreDto>> GetByIdAsync(Guid id);
|
||||
Task<Result<string>> DeleteAsync(Guid id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Services
|
||||
{
|
||||
public interface IJwtService
|
||||
{
|
||||
public Task<string> GenerateJwtTokenAsync(User user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto.Publishers;
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Services
|
||||
{
|
||||
public interface IPublisherService
|
||||
{
|
||||
Task<Result<Publisher>> AddAsync(AddPublisherDto publisher);
|
||||
Task<Result<Publisher>> UpdateAsync(Guid id, AddPublisherDto publisher);
|
||||
Task<Result<string>> DeleteAsync(Guid id);
|
||||
Task<Result<GetPublisherDto>> GetAsync(GetPublisherParamsDto publisherParams);
|
||||
Task<Result<Publisher>> GetByIdAsync(Guid id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Services
|
||||
{
|
||||
public interface IRefreshService
|
||||
{
|
||||
Task<string> GenerateRefreshTokenAsync(User user, string fingerprint, DateTime expires);
|
||||
|
||||
Task<Result<User>> CompareRefreshTokenAsync(string refreshToken, string fingerprint);
|
||||
|
||||
Task<Result<string>> ClearAllRefreshTokensAsync(Guid userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Dto.Auth;
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Interfaces.Services
|
||||
{
|
||||
public interface IUserService
|
||||
{
|
||||
Task<Result<User>> CreateUserAsync(SignUpDto request);
|
||||
Task<Result<User>> GetUserAsync(SignInDto request);
|
||||
Task<Result<UserDto>> GetUserByIdAsync(Guid id);
|
||||
Task<Result<User>> GetUserByEmailAsync(string email);
|
||||
Task VerifyAccountAsync(User user);
|
||||
Task ResetPasswordAsync(User user, string newPassword);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto.Auth;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
|
||||
namespace BookNet.Application.Services
|
||||
{
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IJwtService _jwtService;
|
||||
private readonly IRefreshService _refreshService;
|
||||
private readonly IEmailService _emailService;
|
||||
|
||||
public AuthService(IUserService userService, IJwtService jwtService, IRefreshService refreshService, IEmailService emailService)
|
||||
{
|
||||
_userService = userService;
|
||||
_jwtService = jwtService;
|
||||
_refreshService = refreshService;
|
||||
_emailService = emailService;
|
||||
}
|
||||
|
||||
public async Task<Result<string>> SignUpAsync(SignUpDto request)
|
||||
{
|
||||
var user = await _userService.CreateUserAsync(request);
|
||||
if (!user.IsSuccess || user.Value == null)
|
||||
return Result<string>.Failure(user.Status, user.Errors);
|
||||
|
||||
var result = await SendVerificationCodeAsync(request.Email);
|
||||
if (!result.IsSuccess || result.Value == null)
|
||||
return Result<string>.Failure(user.Status, result.Errors);
|
||||
|
||||
return Result<string>.Success(result.Value);
|
||||
}
|
||||
|
||||
public async Task<Result<AuthDto>> SignInAsync(SignInDto request, string fingerprint)
|
||||
{
|
||||
var user = await _userService.GetUserAsync(request);
|
||||
if (!user.IsSuccess || user.Value == null)
|
||||
return Result<AuthDto>.Failure(user.Status, user.Errors);
|
||||
|
||||
if (!user.Value.EmailConfirmed) return Result<AuthDto>.Failure(403, "Email not confirmed");
|
||||
|
||||
var accessToken = await _jwtService.GenerateJwtTokenAsync(user.Value);
|
||||
var refreshToken = await _refreshService.GenerateRefreshTokenAsync(user.Value, fingerprint, DateTime.UtcNow.AddDays(7));
|
||||
|
||||
var response = new AuthDto() { AccessToken = accessToken, RefreshToken = refreshToken, ExpiresAt = DateTime.UtcNow.AddDays(7) };
|
||||
|
||||
return Result<AuthDto>.Success(response);
|
||||
}
|
||||
|
||||
public async Task<Result<AuthDto>> RefreshTokenAsync(string refreshToken, string fingerprint)
|
||||
{
|
||||
var user = await _refreshService.CompareRefreshTokenAsync(refreshToken, fingerprint);
|
||||
if (!user.IsSuccess || user.Value == null) return Result<AuthDto>.Failure(user.Status, user.Errors);
|
||||
|
||||
var accessToken = await _jwtService.GenerateJwtTokenAsync(user.Value);
|
||||
refreshToken = await _refreshService.GenerateRefreshTokenAsync(user.Value, fingerprint, DateTime.UtcNow.AddDays(7));
|
||||
|
||||
var response = new AuthDto() { AccessToken = accessToken, RefreshToken = refreshToken, ExpiresAt = DateTime.UtcNow.AddDays(7) };
|
||||
|
||||
return Result<AuthDto>.Success(response);
|
||||
}
|
||||
|
||||
public async Task<Result<string>> LogoutAsync(Guid userId)
|
||||
{
|
||||
await _refreshService.ClearAllRefreshTokensAsync(userId);
|
||||
return Result<string>.Success("Success");
|
||||
}
|
||||
|
||||
public async Task<Result<string>> SendVerificationCodeAsync(string email)
|
||||
{
|
||||
var user = await _userService.GetUserByEmailAsync(email);
|
||||
if (!user.IsSuccess || user.Value == null) return Result<string>.Failure(user.Status, user.Errors);
|
||||
|
||||
if (!await _emailService.SendEmailCodeAsync(user.Value, _emailService.GenerateVerificationCode()))
|
||||
return Result<string>.Failure(500, "Failed to send verification code to email");
|
||||
|
||||
return Result<string>.Success("Verification code sent");
|
||||
}
|
||||
|
||||
public async Task<Result<string>> VerifyAccountAsync(string email, string code)
|
||||
{
|
||||
var user = await _userService.GetUserByEmailAsync(email);
|
||||
if (!user.IsSuccess || user.Value == null) return Result<string>.Failure(user.Status, user.Errors);
|
||||
|
||||
if (!await _emailService.CompareEmailCodeAsync(user.Value, code)) return Result<string>.Failure(400, "Invalid code");
|
||||
|
||||
await _userService.VerifyAccountAsync(user.Value);
|
||||
|
||||
return Result<string>.Success("Account verified");
|
||||
}
|
||||
|
||||
public async Task<Result<string>> ResetPasswordAsync(string email, string code, string newPassword)
|
||||
{
|
||||
var user = await _userService.GetUserByEmailAsync(email);
|
||||
if (!user.IsSuccess || user.Value == null) return Result<string>.Failure(user.Status, user.Errors);
|
||||
|
||||
if (!await _emailService.CompareEmailCodeAsync(user.Value, code)) return Result<string>.Failure(400, "Invalid code");
|
||||
|
||||
await _userService.ResetPasswordAsync(user.Value, newPassword);
|
||||
|
||||
return Result<string>.Success("Password changed");
|
||||
}
|
||||
|
||||
public async Task<Result<string>> VerifyCodeAsync(string email, string code)
|
||||
{
|
||||
var user = await _userService.GetUserByEmailAsync(email);
|
||||
if (!user.IsSuccess || user.Value == null) return Result<string>.Failure(user.Status, user.Errors);
|
||||
|
||||
if (!await _emailService.CompareEmailCodeAsync(user.Value, code)) return Result<string>.Failure(400, "Invalid code");
|
||||
|
||||
return Result<string>.Success("Valid code");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto.Authors;
|
||||
using BookNet.Application.Interfaces.Repository;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Services
|
||||
{
|
||||
public class AuthorService : IAuthorService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public AuthorService(IUnitOfWork unitOfWork)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<Author>> AddAsync(AuthorDto author)
|
||||
{
|
||||
Author newAuthor = new Author
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FirstName = author.FirstName,
|
||||
LastName = author.LastName,
|
||||
};
|
||||
if (await _unitOfWork.Authors.Exists(newAuthor)) return Result<Author>.Failure(404, "Author already exists");
|
||||
await _unitOfWork.Authors.AddAsync(newAuthor);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<Author>.Success(newAuthor);
|
||||
}
|
||||
|
||||
public async Task<Result<string>> DeleteAsync(Guid id)
|
||||
{
|
||||
var author = await _unitOfWork.Authors.GetByIdAsync(id);
|
||||
if (author == null) return Result<string>.Failure(404, "Author not found");
|
||||
await _unitOfWork.Authors.DeleteAsync(author);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<string>.Success("Author deleted");
|
||||
}
|
||||
|
||||
public async Task<Result<GetAuthorDto>> GetAsync(GetAuthorParamsDto authorParams)
|
||||
{
|
||||
var authors = await _unitOfWork.Authors.GetAsync();
|
||||
|
||||
if (authors.Count == 0) return Result<GetAuthorDto>.Success(
|
||||
new GetAuthorDto()
|
||||
{
|
||||
Authors = [],
|
||||
Page = authorParams.Page,
|
||||
PageSize = authorParams.PageSize,
|
||||
TotalCount = 0,
|
||||
TotalPages = 0
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(authorParams.Search))
|
||||
authors = authors.Where(a => a.FullName.ToLower().Contains(authorParams.Search.ToLower())).ToList();
|
||||
|
||||
if (authorParams.Sort == "asc")
|
||||
authors = authors.OrderBy(a => a.FullName).ToList();
|
||||
if (authorParams.Sort == "desc")
|
||||
authors = authors.OrderByDescending(a => a.FullName).ToList();
|
||||
|
||||
int totalPages = (int)Math.Ceiling((decimal)authors.Count / authorParams.PageSize);
|
||||
int totalCount = authors.Count;
|
||||
|
||||
if (authorParams.Page > totalPages || authorParams.Page < 1)
|
||||
return Result<GetAuthorDto>.Failure(404, "Запрошенный номер страницы выходит за допустимый диапазон");
|
||||
|
||||
if (authors.Count > authorParams.PageSize)
|
||||
authors = authors.Skip(authorParams.PageSize * (authorParams.Page - 1)).Take(authorParams.PageSize).ToList();
|
||||
|
||||
var result = new GetAuthorDto()
|
||||
{
|
||||
Authors = authors,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages,
|
||||
Page = authorParams.Page,
|
||||
PageSize = authorParams.PageSize
|
||||
};
|
||||
|
||||
return Result<GetAuthorDto>.Success(result);
|
||||
}
|
||||
|
||||
public async Task<Result<Author>> GetByIdAsync(Guid id)
|
||||
{
|
||||
var author = await _unitOfWork.Authors.GetByIdAsync(id);
|
||||
if (author == null) return Result<Author>.Failure(404, "Author not found");
|
||||
return Result<Author>.Success(author);
|
||||
}
|
||||
|
||||
public async Task<Result<Author>> UpdateAsync(Guid id, AuthorDto authorDto)
|
||||
{
|
||||
var author = await _unitOfWork.Authors.GetByIdAsync(id);
|
||||
if (author == null) return Result<Author>.Failure(404, "Author not found");
|
||||
author.FirstName = authorDto.FirstName;
|
||||
author.LastName = authorDto.LastName;
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<Author>.Success(author);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
using AutoMapper;
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Dto.Books;
|
||||
using BookNet.Application.Dto.Genres;
|
||||
using BookNet.Application.Interfaces.Repository;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace BookNet.Application.Services
|
||||
{
|
||||
public class BookService : IBookService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly string _webRootPath;
|
||||
private readonly UserManager<User> _userManager;
|
||||
|
||||
public BookService(IUnitOfWork unitOfWork, IMapper mapper, WebRootOptions options, UserManager<User> userManager)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
_webRootPath = options.WebRootPath;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public async Task<Result<Book>> AddAsync(AddBookDto bookDto)
|
||||
{
|
||||
var author = await _unitOfWork.Authors.GetByIdAsync(bookDto.AuthorId);
|
||||
if (author == null) return Result<Book>.Failure(404, "Author not found");
|
||||
|
||||
var publisher = await _unitOfWork.Publishers.GetByIdAsync(bookDto.PublisherId);
|
||||
if (publisher == null) return Result<Book>.Failure(404, "Publisher not found");
|
||||
|
||||
var genres = await _unitOfWork.Genres.GetByIdsAsync(bookDto.GenreIds);
|
||||
if (genres.Count != bookDto.GenreIds.Length) return Result<Book>.Failure(404, "One or more genres not found");
|
||||
|
||||
Book book = new Book
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Title = bookDto.Title,
|
||||
Description = bookDto.Description,
|
||||
Author = author,
|
||||
Publisher = publisher,
|
||||
Price = bookDto.Price,
|
||||
Pages = bookDto.Pages,
|
||||
ISBN = bookDto.ISBN,
|
||||
ReleaseDate = bookDto.ReleaseDate,
|
||||
Genres = genres
|
||||
};
|
||||
|
||||
if (await _unitOfWork.Books.ExistsAsync(book)) return Result<Book>.Failure(400, "Book already exists");
|
||||
|
||||
string bookFolderPath = Path.Combine(_webRootPath, "books", book.Id.ToString());
|
||||
Directory.CreateDirectory(bookFolderPath);
|
||||
|
||||
if (bookDto.CoverFile != null)
|
||||
{
|
||||
string coverFilePath = Path.Combine(bookFolderPath, "cover.jpg");
|
||||
using (var stream = new FileStream(coverFilePath, FileMode.Create))
|
||||
{
|
||||
await bookDto.CoverFile.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
if (bookDto.BookFile != null)
|
||||
{
|
||||
string bookFilePath = Path.Combine(bookFolderPath, "book.epub");
|
||||
using (var stream = new FileStream(bookFilePath, FileMode.Create))
|
||||
{
|
||||
await bookDto.BookFile.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
await _unitOfWork.Books.AddAsync(book);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<Book>.Success(book);
|
||||
}
|
||||
public async Task<Result<Book>> UpdateAsync(Guid id, AddBookDto bookDto)
|
||||
{
|
||||
var book = await _unitOfWork.Books.GetByIdAsync(id);
|
||||
if (book == null) return Result<Book>.Failure(404, "Book not found");
|
||||
|
||||
var author = await _unitOfWork.Authors.GetByIdAsync(bookDto.AuthorId);
|
||||
if (author == null) return Result<Book>.Failure(404, "Author not found");
|
||||
|
||||
var publisher = await _unitOfWork.Publishers.GetByIdAsync(bookDto.PublisherId);
|
||||
if (publisher == null) return Result<Book>.Failure(404, "Publisher not found");
|
||||
|
||||
var genres = await _unitOfWork.Genres.GetByIdsAsync(bookDto.GenreIds);
|
||||
if (genres.Count != bookDto.GenreIds.Length) return Result<Book>.Failure(404, "One or more genres not found");
|
||||
|
||||
book.Title = bookDto.Title ?? book.Title;
|
||||
book.Description = bookDto.Description ?? book.Description;
|
||||
book.Author = author;
|
||||
book.Publisher = publisher;
|
||||
book.Price = bookDto.Price;
|
||||
book.Pages = bookDto.Pages;
|
||||
book.ISBN = bookDto.ISBN;
|
||||
book.ReleaseDate = bookDto.ReleaseDate;
|
||||
book.Genres = genres;
|
||||
|
||||
string bookFolderPath = Path.Combine(_webRootPath, "books", book.Id.ToString());
|
||||
Directory.CreateDirectory(bookFolderPath);
|
||||
|
||||
if (bookDto.CoverFile != null)
|
||||
{
|
||||
string coverFilePath = Path.Combine(bookFolderPath, "cover.jpg");
|
||||
using (var stream = new FileStream(coverFilePath, FileMode.Create))
|
||||
{
|
||||
await bookDto.CoverFile.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
if (bookDto.BookFile != null)
|
||||
{
|
||||
string bookFilePath = Path.Combine(bookFolderPath, "book.epub");
|
||||
using (var stream = new FileStream(bookFilePath, FileMode.Create))
|
||||
{
|
||||
await bookDto.BookFile.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<Book>.Success(book);
|
||||
}
|
||||
|
||||
public async Task<Result<string>> DeleteAsync(Guid id)
|
||||
{
|
||||
var book = await _unitOfWork.Books.GetByIdAsync(id);
|
||||
if (book == null) return Result<string>.Failure(404, "Book not found");
|
||||
await _unitOfWork.Books.DeleteAsync(book);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<string>.Success("Book deleted");
|
||||
}
|
||||
|
||||
public async Task<Result<GetBookDto>> GetAsync(GetBookParamsDto bookParams)
|
||||
{
|
||||
var books = await _unitOfWork.Books.GetAsync();
|
||||
var booksDto = _mapper.Map<ICollection<BookDto>>(books);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(bookParams.Search))
|
||||
booksDto = booksDto.Where(b => b.Title.ToLower().Replace(" ", "").Contains(bookParams.Search.ToLower().Replace(" ", "")) ||
|
||||
b.Author.FullName.ToLower().Replace(" ", "").Contains(bookParams.Search.ToLower().Replace(" ", "")) ||
|
||||
b.Publisher.Name.ToLower().Replace(" ", "").Contains(bookParams.Search.ToLower().Replace(" ", ""))).ToList();
|
||||
|
||||
if (bookParams.Genres != null)
|
||||
booksDto = booksDto.Where(b => b.Genres.Select(g => g.Id).Any(g => bookParams.Genres.Contains(g))).ToList();
|
||||
|
||||
switch (bookParams.Sort)
|
||||
{
|
||||
case "releaseDesc":
|
||||
booksDto = booksDto.OrderByDescending(b => b.ReleaseDate).ToList();
|
||||
break;
|
||||
case "ratingDesc":
|
||||
booksDto = booksDto.OrderByDescending(b => b.Rating).ToList();
|
||||
break;
|
||||
case "rating":
|
||||
booksDto = booksDto.OrderBy(b => b.Rating).ToList();
|
||||
break;
|
||||
case "priceDesc":
|
||||
booksDto = booksDto.OrderByDescending(b => b.Price).ToList();
|
||||
break;
|
||||
case "price":
|
||||
booksDto = booksDto.OrderBy(b => b.Price).ToList();
|
||||
break;
|
||||
}
|
||||
|
||||
switch (bookParams.Rating)
|
||||
{
|
||||
case "fourHalf":
|
||||
booksDto = booksDto.Where(b => b.Rating >= 4.5).ToList();
|
||||
break;
|
||||
case "four":
|
||||
booksDto = booksDto.Where(b => b.Rating >= 4).ToList();
|
||||
break;
|
||||
}
|
||||
|
||||
if (booksDto.Count == 0) return Result<GetBookDto>.Success(
|
||||
new GetBookDto()
|
||||
{
|
||||
Books = [],
|
||||
Page = bookParams.Page,
|
||||
PageSize = bookParams.PageSize,
|
||||
TotalCount = 0,
|
||||
TotalPages = 0
|
||||
});
|
||||
|
||||
int totalPages = (int)Math.Ceiling((decimal)booksDto.Count / bookParams.PageSize);
|
||||
int totalCount = booksDto.Count;
|
||||
|
||||
if (bookParams.Page > totalPages || bookParams.Page < 1)
|
||||
return Result<GetBookDto>.Failure(404, "Запрошенный номер страницы выходит за допустимый диапазон");
|
||||
|
||||
if (booksDto.Count > bookParams.PageSize)
|
||||
booksDto = booksDto.Skip(bookParams.PageSize * (bookParams.Page - 1)).Take(bookParams.PageSize).ToList();
|
||||
|
||||
foreach (var book in booksDto)
|
||||
{
|
||||
string bookFolderPath = Path.Combine(_webRootPath, "books", book.Id.ToString());
|
||||
string bookFilePath = Path.Combine(bookFolderPath, "book.epub");
|
||||
var epub = new FileInfo(bookFilePath);
|
||||
double mb = (double)epub.Length / (double)1048576;
|
||||
book.Size = $"{mb.ToString("0.00")} MB";
|
||||
}
|
||||
|
||||
var response = new GetBookDto()
|
||||
{
|
||||
Books = booksDto,
|
||||
Page = bookParams.Page,
|
||||
PageSize = bookParams.PageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages
|
||||
};
|
||||
|
||||
return Result<GetBookDto>.Success(response);
|
||||
}
|
||||
|
||||
public async Task<Result<Book>> GetByIdAsync(Guid id)
|
||||
{
|
||||
var book = await _unitOfWork.Books.GetByIdAsync(id);
|
||||
if (book == null) return Result<Book>.Failure(404, "Book not found");
|
||||
return Result<Book>.Success(book);
|
||||
}
|
||||
|
||||
public async Task<Result<BookDto>> GetByIdUserAsync(Guid id, Guid userId)
|
||||
{
|
||||
var book = await _unitOfWork.Books.GetByIdAsync(id);
|
||||
if (book == null) return Result<BookDto>.Failure(404, "Book not found");
|
||||
string bookFolderPath = Path.Combine(_webRootPath, "books", book.Id.ToString());
|
||||
string bookFilePath = Path.Combine(bookFolderPath, "book.epub");
|
||||
var epub = new FileInfo(bookFilePath);
|
||||
book.IsInWishlist = await _unitOfWork.Books.BookInWishlistAsync(userId, book);
|
||||
book.IsPurchased = await _unitOfWork.Books.HasBookAsync(userId, book);
|
||||
BookDto bookDto = _mapper.Map<BookDto>(book);
|
||||
double mb = (double)epub.Length / (double)1048576;
|
||||
bookDto.Size = $"{mb.ToString("0.00")} MB";
|
||||
return Result<BookDto>.Success(bookDto);
|
||||
}
|
||||
|
||||
public async Task<Result<GetBookDto>> GetUserLibraryAsync(Guid userId, GetBookParamsDto bookParams)
|
||||
{
|
||||
var books = await _unitOfWork.Books.GetUserLibraryAsync(userId);
|
||||
|
||||
foreach (var book in books)
|
||||
{
|
||||
book.IsInWishlist = await _unitOfWork.Books.BookInWishlistAsync(userId, book);
|
||||
book.IsPurchased = await _unitOfWork.Books.HasBookAsync(userId, book);
|
||||
}
|
||||
|
||||
var booksDto = _mapper.Map<ICollection<BookDto>>(books);
|
||||
|
||||
if (booksDto.Count == 0) return Result<GetBookDto>.Success(
|
||||
new GetBookDto()
|
||||
{
|
||||
Books = [],
|
||||
Page = bookParams.Page,
|
||||
PageSize = bookParams.PageSize,
|
||||
TotalCount = 0,
|
||||
TotalPages = 0
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(bookParams.Search))
|
||||
booksDto = booksDto.Where(b => b.Title.ToLower().Replace(" ", "").Contains(bookParams.Search.ToLower().Replace(" ", "")) ||
|
||||
b.Author.FullName.ToLower().Replace(" ", "").Contains(bookParams.Search.ToLower().Replace(" ", "")) ||
|
||||
b.Publisher.Name.ToLower().Replace(" ", "").Contains(bookParams.Search.ToLower().Replace(" ", ""))).ToList();
|
||||
|
||||
if (bookParams.Genres != null)
|
||||
booksDto = booksDto.Where(b => b.Genres.Select(g => g.Id).Any(g => bookParams.Genres.Contains(g))).ToList();
|
||||
|
||||
switch (bookParams.Sort)
|
||||
{
|
||||
case "releaseDesc":
|
||||
booksDto = booksDto.OrderByDescending(b => b.ReleaseDate).ToList();
|
||||
break;
|
||||
case "ratingDesc":
|
||||
booksDto = booksDto.OrderByDescending(b => b.Rating).ToList();
|
||||
break;
|
||||
case "rating":
|
||||
booksDto = booksDto.OrderBy(b => b.Rating).ToList();
|
||||
break;
|
||||
case "priceDesc":
|
||||
booksDto = booksDto.OrderByDescending(b => b.Price).ToList();
|
||||
break;
|
||||
case "price":
|
||||
booksDto = booksDto.OrderBy(b => b.Price).ToList();
|
||||
break;
|
||||
}
|
||||
|
||||
switch (bookParams.Rating)
|
||||
{
|
||||
case "fourHalf":
|
||||
booksDto = booksDto.Where(b => b.Rating >= 4.5).ToList();
|
||||
break;
|
||||
case "four":
|
||||
booksDto = booksDto.Where(b => b.Rating >= 4).ToList();
|
||||
break;
|
||||
}
|
||||
|
||||
int totalPages = (int)Math.Ceiling((decimal)booksDto.Count / bookParams.PageSize);
|
||||
int totalCount = booksDto.Count;
|
||||
|
||||
if (bookParams.Page > totalPages || bookParams.Page < 1)
|
||||
return Result<GetBookDto>.Failure(404, "Запрошенный номер страницы выходит за допустимый диапазон");
|
||||
|
||||
if (booksDto.Count > bookParams.PageSize)
|
||||
booksDto = booksDto.Skip(bookParams.PageSize * (bookParams.Page - 1)).Take(bookParams.PageSize).ToList();
|
||||
|
||||
foreach (var book in booksDto)
|
||||
{
|
||||
string bookFolderPath = Path.Combine(_webRootPath, "books", book.Id.ToString());
|
||||
string bookFilePath = Path.Combine(bookFolderPath, "book.epub");
|
||||
var epub = new FileInfo(bookFilePath);
|
||||
double mb = (double)epub.Length / (double)1048576;
|
||||
book.Size = $"{mb.ToString("0.00")} MB";
|
||||
}
|
||||
|
||||
var response = new GetBookDto()
|
||||
{
|
||||
Books = booksDto,
|
||||
Page = bookParams.Page,
|
||||
PageSize = bookParams.PageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages
|
||||
};
|
||||
|
||||
return Result<GetBookDto>.Success(response);
|
||||
}
|
||||
|
||||
public async Task<Result<GetBookDto>> GetUserWishlistAsync(Guid userId, GetBookParamsDto bookParams)
|
||||
{
|
||||
var books = await _unitOfWork.Books.GetUserWishlistAsync(userId);
|
||||
|
||||
foreach (var book in books)
|
||||
{
|
||||
book.IsInWishlist = await _unitOfWork.Books.BookInWishlistAsync(userId, book);
|
||||
book.IsPurchased = await _unitOfWork.Books.HasBookAsync(userId, book);
|
||||
}
|
||||
|
||||
var booksDto = _mapper.Map<ICollection<BookDto>>(books);
|
||||
|
||||
if (booksDto.Count == 0) return Result<GetBookDto>.Success(
|
||||
new GetBookDto()
|
||||
{
|
||||
Books = [],
|
||||
Page = bookParams.Page,
|
||||
PageSize = bookParams.PageSize,
|
||||
TotalCount = 0,
|
||||
TotalPages = 0
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(bookParams.Search))
|
||||
booksDto = booksDto.Where(b => b.Title.ToLower().Replace(" ", "").Contains(bookParams.Search.ToLower().Replace(" ", "")) ||
|
||||
b.Author.FullName.ToLower().Replace(" ", "").Contains(bookParams.Search.ToLower().Replace(" ", "")) ||
|
||||
b.Publisher.Name.ToLower().Replace(" ", "").Contains(bookParams.Search.ToLower().Replace(" ", ""))).ToList();
|
||||
|
||||
if (bookParams.Genres != null)
|
||||
booksDto = booksDto.Where(b => b.Genres.Select(g => g.Id).Any(g => bookParams.Genres.Contains(g))).ToList();
|
||||
|
||||
switch (bookParams.Sort)
|
||||
{
|
||||
case "releaseDesc":
|
||||
booksDto = booksDto.OrderByDescending(b => b.ReleaseDate).ToList();
|
||||
break;
|
||||
case "ratingDesc":
|
||||
booksDto = booksDto.OrderByDescending(b => b.Rating).ToList();
|
||||
break;
|
||||
case "rating":
|
||||
booksDto = booksDto.OrderBy(b => b.Rating).ToList();
|
||||
break;
|
||||
case "priceDesc":
|
||||
booksDto = booksDto.OrderByDescending(b => b.Price).ToList();
|
||||
break;
|
||||
case "price":
|
||||
booksDto = booksDto.OrderBy(b => b.Price).ToList();
|
||||
break;
|
||||
}
|
||||
|
||||
switch (bookParams.Rating)
|
||||
{
|
||||
case "fourHalf":
|
||||
booksDto = booksDto.Where(b => b.Rating >= 4.5).ToList();
|
||||
break;
|
||||
case "four":
|
||||
booksDto = booksDto.Where(b => b.Rating >= 4).ToList();
|
||||
break;
|
||||
}
|
||||
|
||||
int totalPages = (int)Math.Ceiling((decimal)booksDto.Count / bookParams.PageSize);
|
||||
int totalCount = booksDto.Count;
|
||||
|
||||
if (bookParams.Page > totalPages || bookParams.Page < 1)
|
||||
return Result<GetBookDto>.Failure(404, "Запрошенный номер страницы выходит за допустимый диапазон");
|
||||
|
||||
if (booksDto.Count > bookParams.PageSize)
|
||||
booksDto = booksDto.Skip(bookParams.PageSize * (bookParams.Page - 1)).Take(bookParams.PageSize).ToList();
|
||||
|
||||
foreach (var book in booksDto)
|
||||
{
|
||||
string bookFolderPath = Path.Combine(_webRootPath, "books", book.Id.ToString());
|
||||
string bookFilePath = Path.Combine(bookFolderPath, "book.epub");
|
||||
var epub = new FileInfo(bookFilePath);
|
||||
double mb = (double)epub.Length / (double)1048576;
|
||||
book.Size = $"{mb.ToString("0.00")} MB";
|
||||
}
|
||||
|
||||
var response = new GetBookDto()
|
||||
{
|
||||
Books = booksDto,
|
||||
Page = bookParams.Page,
|
||||
PageSize = bookParams.PageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages
|
||||
};
|
||||
|
||||
return Result<GetBookDto>.Success(response);
|
||||
}
|
||||
|
||||
public async Task<Result<string>> ToggleWishlist(Guid userId, Guid bookId)
|
||||
{
|
||||
var book = await _unitOfWork.Books.GetByIdAsync(bookId);
|
||||
if (book == null) return Result<string>.Failure(404, "Book not found");
|
||||
|
||||
var bookUser = book.Users.FirstOrDefault(b => b.Book == book && b.UserId == userId);
|
||||
|
||||
if (bookUser != null && bookUser.IsPurchased)
|
||||
return Result<string>.Failure(400, "Book is purchased");
|
||||
|
||||
if (bookUser != null)
|
||||
book.Users.Remove(bookUser);
|
||||
else
|
||||
book.Users.Add(new BookUser() { UserId = userId, Book = book, IsPurchased = false });
|
||||
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<string>.Success("Success");
|
||||
}
|
||||
|
||||
public async Task<Result<string>> BuyBook(Guid userId, Guid bookId)
|
||||
{
|
||||
var book = await _unitOfWork.Books.GetByIdAsync(bookId);
|
||||
if (book == null) return Result<string>.Failure(404, "Book not found");
|
||||
|
||||
if(await _unitOfWork.Books.HasBookAsync(userId, book))
|
||||
return Result<string>.Failure(400, "User already owns this book");
|
||||
|
||||
var bookUser = book.Users.FirstOrDefault(b => b.Book == book && b.UserId == userId);
|
||||
|
||||
if (bookUser == null)
|
||||
book.Users.Add(new BookUser() { UserId = userId, Book = book, IsPurchased = true });
|
||||
else
|
||||
bookUser.IsPurchased = true;
|
||||
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<string>.Success("Success");
|
||||
}
|
||||
|
||||
public async Task<Result<string>> DownloadBook(Guid userId, Guid bookId)
|
||||
{
|
||||
var book = await _unitOfWork.Books.GetByIdAsync(bookId);
|
||||
if (book == null) return Result<string>.Failure(404, "Book not found");
|
||||
|
||||
if (!await _unitOfWork.Books.HasBookAsync(userId, book))
|
||||
return Result<string>.Failure(400, "User don't owns this book");
|
||||
|
||||
string bookFolderPath = Path.Combine(_webRootPath, "books", book.Id.ToString());
|
||||
string bookFilePath = Path.Combine(bookFolderPath, "book.epub");
|
||||
|
||||
if (!File.Exists(bookFilePath))
|
||||
return Result<string>.Failure(500, "Book file not found");
|
||||
|
||||
return Result<string>.Success(bookFilePath);
|
||||
}
|
||||
|
||||
public async Task<Result<ReviewDto>> AddReview(Guid userId, Guid bookId, WriteReviewDto reviewDto)
|
||||
{
|
||||
var book = await _unitOfWork.Books.GetByIdAsync(bookId);
|
||||
if (book == null) return Result<ReviewDto>.Failure(404, "Book not found");
|
||||
|
||||
if (!await _unitOfWork.Books.HasBookAsync(userId, book))
|
||||
return Result<ReviewDto>.Failure(400, "Only users who have purchased this book can leave reviews");
|
||||
|
||||
var review = book.Reviews.FirstOrDefault(r => r.Author.Id == userId);
|
||||
if (review != null)
|
||||
await _unitOfWork.Reviews.DeleteAsync(review);
|
||||
|
||||
review = new Review()
|
||||
{
|
||||
AuthorId = userId,
|
||||
BookId = book.Id,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
Text = reviewDto.Text,
|
||||
Stars = reviewDto.Stars
|
||||
};
|
||||
|
||||
await _unitOfWork.Reviews.AddAsync(review);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
return Result<ReviewDto>.Success(_mapper.Map<ReviewDto>(review));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using BookNet.Application.Interfaces.Repository;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
using System.Text;
|
||||
|
||||
namespace BookNet.Application.Services
|
||||
{
|
||||
public class ConsoleEmailService : IEmailService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
public ConsoleEmailService(IUnitOfWork unitOfWork, ICryptoService cryptoService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_cryptoService = cryptoService;
|
||||
}
|
||||
|
||||
public string GenerateVerificationCode()
|
||||
{
|
||||
Random random = new Random();
|
||||
const int codeLength = 4;
|
||||
StringBuilder codeBuilder = new StringBuilder(codeLength);
|
||||
|
||||
for (int i = 0; i < codeLength; i++)
|
||||
codeBuilder.Append(random.Next(0, 10));
|
||||
|
||||
return codeBuilder.ToString();
|
||||
}
|
||||
|
||||
public async Task<bool> SendEmailCodeAsync(User user, string code)
|
||||
{
|
||||
var emailCode = new EmailCode()
|
||||
{
|
||||
User = user,
|
||||
Code = _cryptoService.Hash(code, Convert.FromBase64String(user.Salt)),
|
||||
ExpirationDate = DateTime.UtcNow.AddMinutes(5)
|
||||
};
|
||||
await _unitOfWork.EmailCodes.AddAsync(emailCode);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine($"Code for verification {user.Email}: {code}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> CompareEmailCodeAsync(User user, string code)
|
||||
{
|
||||
var emailCode = await _unitOfWork.EmailCodes.GetAsync(user.Id);
|
||||
if (emailCode == null || emailCode.ExpirationDate < DateTime.UtcNow)
|
||||
return false;
|
||||
|
||||
if (_cryptoService.Hash(code, Convert.FromBase64String(user.Salt)) != emailCode.Code)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace BookNet.Application.Services
|
||||
{
|
||||
public class CryptoService : ICryptoService
|
||||
{
|
||||
//Константы для хэширования
|
||||
private const int SaltSize = 32;
|
||||
private const int KeySize = 32;
|
||||
private const int Iterations = 100_000;
|
||||
|
||||
public byte[] GenerateSalt()
|
||||
{
|
||||
var salt = new byte[SaltSize];
|
||||
RandomNumberGenerator.Fill(salt);
|
||||
return salt;
|
||||
}
|
||||
|
||||
public string Hash(string text, byte[] salt)
|
||||
{
|
||||
var pbkdf2 = Rfc2898DeriveBytes.Pbkdf2(text, salt, Iterations,
|
||||
HashAlgorithmName.SHA256, KeySize);
|
||||
return Convert.ToBase64String(pbkdf2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using AutoMapper;
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto.Genres;
|
||||
using BookNet.Application.Interfaces.Repository;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Services
|
||||
{
|
||||
public class GenreService : IGenreService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly string _webRootPath;
|
||||
public GenreService(IUnitOfWork unitOfWork, IMapper mapper, WebRootOptions options)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
_webRootPath = options.WebRootPath;
|
||||
}
|
||||
|
||||
public async Task<Result<GenreDto>> AddAsync(AddGenreDto genreDto)
|
||||
{
|
||||
Genre genre = new Genre() { Id = Guid.NewGuid(), Name = genreDto.Name };
|
||||
if (await _unitOfWork.Genres.Exists(genre)) return Result<GenreDto>.Failure(400, "Genre already exists");
|
||||
await _unitOfWork.Genres.AddAsync(genre);
|
||||
|
||||
if (genreDto.ImageFile != null)
|
||||
{
|
||||
var genresDir = Path.Combine(_webRootPath, "genres");
|
||||
Directory.CreateDirectory(genresDir);
|
||||
string imagePath = Path.Combine(genresDir, $"{genre.Id}.jpg");
|
||||
using (var stream = new FileStream(imagePath, FileMode.Create))
|
||||
{
|
||||
await genreDto.ImageFile.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<GenreDto>.Success(_mapper.Map<GenreDto>(genre));
|
||||
}
|
||||
|
||||
public async Task<Result<string>> DeleteAsync(Guid id)
|
||||
{
|
||||
var genre = await _unitOfWork.Genres.GetByIdAsync(id);
|
||||
if (genre == null) return Result<string>.Failure(404, "Genre not found");
|
||||
|
||||
string imagePath = Path.Combine(_webRootPath, "genres", $"{genre.Id}.jpg");
|
||||
if (File.Exists(imagePath)) File.Delete(imagePath);
|
||||
|
||||
|
||||
await _unitOfWork.Genres.DeleteAsync(genre);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<string>.Success("Genre deleted");
|
||||
}
|
||||
|
||||
public async Task<Result<GetGenreDto>> GetAsync(GetGenreParamsDto genreParams)
|
||||
{
|
||||
var genres = await _unitOfWork.Genres.GetAsync();
|
||||
if (genres.Count == 0) return Result<GetGenreDto>.Success(
|
||||
new GetGenreDto()
|
||||
{
|
||||
Genres = [],
|
||||
Page = genreParams.Page,
|
||||
PageSize = genreParams.PageSize,
|
||||
TotalCount = 0,
|
||||
TotalPages = 0
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genreParams.Search))
|
||||
genres = genres.Where(a => a.Name.ToLower().Contains(genreParams.Search.ToLower())).ToList();
|
||||
|
||||
if (genreParams.Sort == "asc")
|
||||
genres = genres.OrderBy(a => a.Name).ToList();
|
||||
if (genreParams.Sort == "desc")
|
||||
genres = genres.OrderByDescending(a => a.Name).ToList();
|
||||
|
||||
int totalPages = (int)Math.Ceiling((decimal)genres.Count / genreParams.PageSize);
|
||||
int totalCount = genres.Count;
|
||||
|
||||
if (genreParams.Page > totalPages || genreParams.Page < 1)
|
||||
return Result<GetGenreDto>.Failure(404, "Запрошенный номер страницы выходит за допустимый диапазон");
|
||||
|
||||
if (genres.Count > genreParams.PageSize)
|
||||
genres = genres.Skip(genreParams.PageSize * (genreParams.Page - 1)).Take(genreParams.PageSize).ToList();
|
||||
|
||||
var result = new GetGenreDto()
|
||||
{
|
||||
Genres = _mapper.Map<ICollection<GenreDto>>(genres),
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages,
|
||||
Page = genreParams.Page,
|
||||
PageSize = genreParams.PageSize
|
||||
};
|
||||
|
||||
return Result<GetGenreDto>.Success(result);
|
||||
}
|
||||
|
||||
public async Task<Result<GenreDto>> GetByIdAsync(Guid id)
|
||||
{
|
||||
var genre = await _unitOfWork.Genres.GetByIdAsync(id);
|
||||
if (genre == null) return Result<GenreDto>.Failure(404, "Genre not found");
|
||||
return Result<GenreDto>.Success(_mapper.Map<GenreDto>(genre));
|
||||
}
|
||||
|
||||
public async Task<Result<GenreDto>> UpdateAsync(Guid id, AddGenreDto genreDto)
|
||||
{
|
||||
var genre = await _unitOfWork.Genres.GetByIdAsync(id);
|
||||
if (genre == null) return Result<GenreDto>.Failure(404, "Genre not found");
|
||||
genre.Name = genreDto.Name;
|
||||
|
||||
if (genreDto.ImageFile != null)
|
||||
{
|
||||
var genresDir = Path.Combine(_webRootPath, "genres");
|
||||
Directory.CreateDirectory(genresDir);
|
||||
string imagePath = Path.Combine(genresDir, $"{genre.Id}.jpg");
|
||||
using (var stream = new FileStream(imagePath, FileMode.OpenOrCreate))
|
||||
{
|
||||
await genreDto.ImageFile.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<GenreDto>.Success(_mapper.Map<GenreDto>(genre));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace BookNet.Application.Services
|
||||
{
|
||||
public class JwtService : IJwtService
|
||||
{
|
||||
private readonly JwtSettings _jwtSettings;
|
||||
private readonly UserManager<User> _userManager;
|
||||
public JwtService(IOptions<JwtSettings> jwtSettings, UserManager<User> userManager)
|
||||
{
|
||||
_jwtSettings = jwtSettings.Value;
|
||||
_userManager = userManager;
|
||||
}
|
||||
public async Task<string> GenerateJwtTokenAsync(User user)
|
||||
{
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
|
||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new List<Claim>()
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new(ClaimTypes.Email, user.Email ?? ""),
|
||||
};
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _jwtSettings.Issuer,
|
||||
audience: _jwtSettings.Audience,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddMinutes(_jwtSettings.AccessTokenLifetimeMinutes),
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto.Publishers;
|
||||
using BookNet.Application.Interfaces.Repository;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
|
||||
namespace BookNet.Application.Services
|
||||
{
|
||||
public class PublisherService : IPublisherService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public PublisherService(IUnitOfWork unitOfWork)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<Publisher>> AddAsync(AddPublisherDto publisher)
|
||||
{
|
||||
Publisher newPublisher = new Publisher
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = publisher.Name
|
||||
};
|
||||
|
||||
if (await _unitOfWork.Publishers.Exists(newPublisher)) return Result<Publisher>.Failure(400, "Publisher already exists");
|
||||
await _unitOfWork.Publishers.AddAsync(newPublisher);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<Publisher>.Success(newPublisher);
|
||||
}
|
||||
|
||||
public async Task<Result<string>> DeleteAsync(Guid id)
|
||||
{
|
||||
var publisher = await _unitOfWork.Publishers.GetByIdAsync(id);
|
||||
if (publisher == null) return Result<string>.Failure(404, "Publisher not found");
|
||||
await _unitOfWork.Publishers.DeleteAsync(publisher);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<string>.Success("Publisher deleted");
|
||||
}
|
||||
|
||||
public async Task<Result<GetPublisherDto>> GetAsync(GetPublisherParamsDto publisherParams)
|
||||
{
|
||||
var publishers = await _unitOfWork.Publishers.GetAsync();
|
||||
|
||||
if (publishers.Count == 0) return Result<GetPublisherDto>.Success(
|
||||
new GetPublisherDto()
|
||||
{
|
||||
Publishers = [],
|
||||
Page = publisherParams.Page,
|
||||
PageSize = publisherParams.PageSize,
|
||||
TotalCount = 0,
|
||||
TotalPages = 0
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(publisherParams.Search))
|
||||
publishers = publishers.Where(a => a.Name.ToLower().Contains(publisherParams.Search.ToLower())).ToList();
|
||||
|
||||
if (publisherParams.Sort == "asc")
|
||||
publishers = publishers.OrderBy(a => a.Name).ToList();
|
||||
if (publisherParams.Sort == "desc")
|
||||
publishers = publishers.OrderByDescending(a => a.Name).ToList();
|
||||
|
||||
int totalPages = (int)Math.Ceiling((decimal)publishers.Count / publisherParams.PageSize);
|
||||
int totalCount = publishers.Count;
|
||||
|
||||
if (publisherParams.Page > totalPages || publisherParams.Page < 1)
|
||||
return Result<GetPublisherDto>.Failure(404, "Запрошенный номер страницы выходит за допустимый диапазон");
|
||||
|
||||
if (publishers.Count > publisherParams.PageSize)
|
||||
publishers = publishers.Skip(publisherParams.PageSize * (publisherParams.Page - 1)).Take(publisherParams.PageSize).ToList();
|
||||
|
||||
var result = new GetPublisherDto()
|
||||
{
|
||||
Publishers = publishers,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages,
|
||||
Page = publisherParams.Page,
|
||||
PageSize = publisherParams.PageSize
|
||||
};
|
||||
|
||||
return Result<GetPublisherDto>.Success(result);
|
||||
}
|
||||
|
||||
public async Task<Result<Publisher>> GetByIdAsync(Guid id)
|
||||
{
|
||||
var publisher = await _unitOfWork.Publishers.GetByIdAsync(id);
|
||||
if (publisher == null) return Result<Publisher>.Failure(404, "Publisher not found");
|
||||
return Result<Publisher>.Success(publisher);
|
||||
}
|
||||
|
||||
public async Task<Result<Publisher>> UpdateAsync(Guid id, AddPublisherDto publisherDto)
|
||||
{
|
||||
var publisher = await _unitOfWork.Publishers.GetByIdAsync(id);
|
||||
if (publisher == null) return Result<Publisher>.Failure(404, "Publisher not found");
|
||||
publisher.Name = publisherDto.Name;
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
return Result<Publisher>.Success(publisher);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Interfaces.Repository;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace BookNet.Application.Services
|
||||
{
|
||||
public class RefreshService : IRefreshService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly UserManager<User> _userManager;
|
||||
|
||||
public RefreshService(IUnitOfWork unitOfWork, ICryptoService cryptoService, UserManager<User> userManager)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_cryptoService = cryptoService;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateRefreshTokenAsync(User user, string fingerprint,
|
||||
DateTime expires)
|
||||
{
|
||||
//Генерируем токен
|
||||
var bytes = new byte[64];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
var refreshToken = Convert.ToHexString(bytes);
|
||||
|
||||
//Создаём модель
|
||||
var salt = Convert.FromBase64String(user.Salt);
|
||||
var token = new RefreshToken()
|
||||
{
|
||||
Token = refreshToken, //_cryptoService.Hash(refreshToken, salt)
|
||||
UserId = user.Id,
|
||||
ExpiresAt = expires,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
IsRevoked = false,
|
||||
FingerPrint = _cryptoService.Hash(fingerprint, salt)
|
||||
};
|
||||
|
||||
//Сохраняем в бд
|
||||
await _unitOfWork.RefreshTokens.AddAsync(token);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
return refreshToken;
|
||||
}
|
||||
public async Task<Result<User>> CompareRefreshTokenAsync(string refreshToken, string fingerprint)
|
||||
{
|
||||
//Получаем текущий refresh token
|
||||
var token = await _unitOfWork.RefreshTokens.GetByHashAsync(refreshToken);
|
||||
if (token == null)
|
||||
return Result<User>.Failure(401, "Запрошенный refresh token не найден");
|
||||
|
||||
var user = await _userManager.FindByIdAsync(token.UserId.ToString());
|
||||
if (user == null)
|
||||
return Result<User>.Failure(401, "Запрошенный пользователь не найден");
|
||||
|
||||
//Проверяем fingerprint
|
||||
if (token.FingerPrint != _cryptoService.Hash(fingerprint, Convert.FromBase64String(user.Salt)))
|
||||
return Result<User>.Failure(401, "Поменялось окружение, попробуйте авторизоваться снова");
|
||||
|
||||
//Удаляем его и сохраняем бд
|
||||
await _unitOfWork.RefreshTokens.DeleteAsync(token);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
//Проверяем срок действия
|
||||
if (token.ExpiresAt < DateTime.UtcNow || token.IsRevoked || user == null)
|
||||
return Result<User>.Failure(401, "Срок действия запрошенного refresh token истёк или он отозван");
|
||||
|
||||
return Result<User>.Success(user);
|
||||
}
|
||||
|
||||
public async Task<Result<string>> ClearAllRefreshTokensAsync(Guid userId)
|
||||
{
|
||||
//Удаляем все токены и сохраняем бд
|
||||
await _unitOfWork.RefreshTokens.DeleteUserTokensAsync(userId);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
return Result<string>.Success("Refresh token пользователя были удалены");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using AutoMapper;
|
||||
using BookNet.Application.Common;
|
||||
using BookNet.Application.Dto;
|
||||
using BookNet.Application.Dto.Auth;
|
||||
using BookNet.Application.Interfaces.Repository;
|
||||
using BookNet.Application.Interfaces.Services;
|
||||
using BookNet.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace BookNet.Application.Services
|
||||
{
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly RoleManager<IdentityRole<Guid>> _roleManager;
|
||||
|
||||
public UserService(IUnitOfWork unitOfWork,
|
||||
ICryptoService cryptoService,
|
||||
UserManager<User> userManager,
|
||||
RoleManager<IdentityRole<Guid>> roleManager,
|
||||
IMapper mapper)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_cryptoService = cryptoService;
|
||||
_userManager = userManager;
|
||||
_roleManager = roleManager;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<Result<User>> CreateUserAsync(SignUpDto request)
|
||||
{
|
||||
var exist = await _userManager.FindByEmailAsync(request.Email);
|
||||
if (exist != null) return Result<User>.Failure(400, "Email already exists");
|
||||
|
||||
var user = new User()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserName = request.Email,
|
||||
Email = request.Email,
|
||||
FirstName = request.FirstName,
|
||||
LastName = request.LastName,
|
||||
RegistrationDate = DateTime.UtcNow,
|
||||
Salt = Convert.ToBase64String(_cryptoService.GenerateSalt())
|
||||
};
|
||||
|
||||
var result = await _userManager.CreateAsync(user, request.Password);
|
||||
if (!result.Succeeded) return Result<User>.Failure(400, result.Errors.Select(e => e.Description).ToArray());
|
||||
|
||||
const string defaultRole = "User";
|
||||
if (!await _roleManager.RoleExistsAsync(defaultRole))
|
||||
{
|
||||
await _roleManager.CreateAsync(new IdentityRole<Guid>("User"));
|
||||
await _roleManager.CreateAsync(new IdentityRole<Guid>("Admin"));
|
||||
}
|
||||
await _userManager.AddToRoleAsync(user, defaultRole);
|
||||
|
||||
return Result<User>.Success(user);
|
||||
}
|
||||
|
||||
public async Task<Result<User>> GetUserAsync(SignInDto request)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(request.Email);
|
||||
if (user == null) return Result<User>.Failure(400, "Invalid credentials");
|
||||
|
||||
if (!await _userManager.CheckPasswordAsync(user, request.Password))
|
||||
return Result<User>.Failure(400, "Invalid credentials");
|
||||
|
||||
return Result<User>.Success(user);
|
||||
}
|
||||
|
||||
public async Task<Result<User>> GetUserByEmailAsync(string email)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(email);
|
||||
if (user == null) return Result<User>.Failure(400, "Invalid email");
|
||||
|
||||
return Result<User>.Success(user);
|
||||
}
|
||||
|
||||
public async Task VerifyAccountAsync(User user)
|
||||
{
|
||||
user.EmailConfirmed = true;
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task ResetPasswordAsync(User user, string newPassword)
|
||||
{
|
||||
await _userManager.ResetPasswordAsync(user, await _userManager.GeneratePasswordResetTokenAsync(user), newPassword);
|
||||
}
|
||||
|
||||
public async Task<Result<UserDto>> GetUserByIdAsync(Guid id)
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(id.ToString());
|
||||
if (user == null) return Result<UserDto>.Failure(404, "User not found");
|
||||
return Result<UserDto>.Success(_mapper.Map<UserDto>(user));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.2.11415.280
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookNet.API", "BookNet.API\BookNet.API.csproj", "{2D6ED1D0-5FB3-4D94-4738-1859C19E934C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookNet.Domain", "BookNet.Domain\BookNet.Domain.csproj", "{1F5F3C42-895F-4CD6-BC4D-ED66A096C7B8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookNet.Infrastructure", "BookNet.Infrastructure\BookNet.Infrastructure.csproj", "{49EED285-B199-4F95-83A3-4CB09AD3B12D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookNet.Application", "BookNet.Application\BookNet.Application.csproj", "{2F63B48F-1AFF-4A03-A974-EC1AA2642200}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2D6ED1D0-5FB3-4D94-4738-1859C19E934C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2D6ED1D0-5FB3-4D94-4738-1859C19E934C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2D6ED1D0-5FB3-4D94-4738-1859C19E934C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2D6ED1D0-5FB3-4D94-4738-1859C19E934C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1F5F3C42-895F-4CD6-BC4D-ED66A096C7B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1F5F3C42-895F-4CD6-BC4D-ED66A096C7B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1F5F3C42-895F-4CD6-BC4D-ED66A096C7B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1F5F3C42-895F-4CD6-BC4D-ED66A096C7B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{49EED285-B199-4F95-83A3-4CB09AD3B12D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{49EED285-B199-4F95-83A3-4CB09AD3B12D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{49EED285-B199-4F95-83A3-4CB09AD3B12D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{49EED285-B199-4F95-83A3-4CB09AD3B12D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2F63B48F-1AFF-4A03-A974-EC1AA2642200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2F63B48F-1AFF-4A03-A974-EC1AA2642200}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2F63B48F-1AFF-4A03-A974-EC1AA2642200}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2F63B48F-1AFF-4A03-A974-EC1AA2642200}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {18D33B15-98C5-4769-BE02-628773E8CE9D}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||