From 2f2833e43186c9274fe11e114bc49be319f8bbb4 Mon Sep 17 00:00:00 2001 From: Matheus Ribeiro Date: Wed, 27 Mar 2024 12:27:17 -0300 Subject: [PATCH] add projects --- .gitignore | 485 +++++++++++++++++ ParkingLotManager.ReportApi/Configuration.cs | 8 + .../Controller/ReportController.cs | 62 +++ .../DTOs/ParkingLotGenericResponseDTO.cs | 36 ++ .../DTOs/VehicleModelDto.cs | 15 + .../Interfaces/IVehicleFlowManagement.cs | 13 + .../Interfaces/IVehicleQuery.cs | 9 + .../Mappings/VehicleMapping.cs | 15 + .../Models/VehicleModel.cs | 21 + .../ParkingLotManager.ReportApi.csproj | 20 + .../ParkingLotManager.ReportApi.http | 6 + .../ParkingLotManager.ReportApi.xml | 14 + ParkingLotManager.ReportApi/Program.cs | 72 +++ .../Properties/launchSettings.json | 41 ++ .../Services/FlowManagementService.cs | 106 ++++ .../ParkingLotManagerWebApiService.cs | 44 ++ .../appsettings.Development.json | 8 + ParkingLotManager.ReportApi/appsettings.json | 9 + ParkingLotManager.WebApi/.gitignore | 489 ++++++++++++++++++ .../Attributes/ApiKeyAttribute.cs | 53 ++ ParkingLotManager.WebApi/Configuration.cs | 20 + .../Controllers/AccountController.cs | 315 +++++++++++ .../Controllers/CompanyController.cs | 236 +++++++++ .../Controllers/HomeController.cs | 47 ++ .../Controllers/VehicleController.cs | 344 ++++++++++++ ParkingLotManager.WebApi/DTOs/CompanyDTO.cs | 32 ++ .../DTOs/Mappings/MappingDTOs.cs | 14 + ParkingLotManager.WebApi/DTOs/UserDTO.cs | 51 ++ ParkingLotManager.WebApi/DTOs/VehicleDTO.cs | 56 ++ .../Data/AppDataContext.cs | 57 ++ .../Data/Mappings/CompanyMap.cs | 69 +++ .../Data/Mappings/UserMap.cs | 70 +++ .../Data/Mappings/VehicleMap.cs | 71 +++ .../Enums/EVehicleType.cs | 7 + .../Extensions/EntitiesExtensions.cs | 34 ++ .../Extensions/ModelStateExtension.cs | 14 + .../Extensions/RoleClaimsExtension.cs | 22 + .../Extensions/StringExtension.cs | 15 + ParkingLotManager.WebApi/Models/Company.cs | 50 ++ .../Models/Contracts/ICompany.cs | 11 + .../Models/Contracts/IUser.cs | 12 + .../Models/Contracts/IVehicle.cs | 10 + ParkingLotManager.WebApi/Models/Role.cs | 13 + ParkingLotManager.WebApi/Models/User.cs | 55 ++ ParkingLotManager.WebApi/Models/Vehicle.cs | 69 +++ .../ParkingLotManager.WebApi.csproj | 38 ++ .../ParkingLotManager.WebApi.http | 6 + .../ParkingLotManagerWebApi.xml | 233 +++++++++ ParkingLotManager.WebApi/Program.cs | 118 +++++ .../Properties/launchSettings.json | 41 ++ .../Services/Contracts/ITokenService.cs | 8 + .../Services/TokenService.cs | 31 ++ .../ValueObjects/Address.cs | 30 ++ ParkingLotManager.WebApi/ValueObjects/Cnpj.cs | 24 + .../ValueObjects/ValueObject.cs | 5 + .../RegisterCompanyViewModel.cs | 47 ++ .../UpdateCompanyViewModel.cs | 45 ++ .../ViewModels/ResultViewModel.cs | 30 ++ .../UserViewModels/CreateUserViewModel.cs | 23 + .../UserViewModels/LoginViewModel.cs | 14 + .../UserViewModels/UpdateUserViewModel.cs | 26 + .../RegisterVehicleViewModel.cs | 44 ++ .../UpdateVehicleViewModel.cs | 45 ++ .../appsettings.Development.json | 14 + ParkingLotManager.WebApi/appsettings.json | 9 + .../Controllers/AccountControllerTests.cs | 92 ++++ .../Controllers/CompanyControllerTests.cs | 94 ++++ .../Controllers/VehicleControllerTests.cs | 99 ++++ .../Entities/CompanyTests.cs | 48 ++ .../Entities/UserTests.cs | 52 ++ .../Entities/VehicleTests.cs | 61 +++ .../ParkingLotManager.XUnitTests.csproj | 24 + .../Services/FlowManagementServiceTests.cs | 25 + .../Services/TokenServiceTests.cs | 27 + dotnet-test.sln | 62 +++ 75 files changed, 4635 insertions(+) create mode 100644 .gitignore create mode 100644 ParkingLotManager.ReportApi/Configuration.cs create mode 100644 ParkingLotManager.ReportApi/Controller/ReportController.cs create mode 100644 ParkingLotManager.ReportApi/DTOs/ParkingLotGenericResponseDTO.cs create mode 100644 ParkingLotManager.ReportApi/DTOs/VehicleModelDto.cs create mode 100644 ParkingLotManager.ReportApi/Interfaces/IVehicleFlowManagement.cs create mode 100644 ParkingLotManager.ReportApi/Interfaces/IVehicleQuery.cs create mode 100644 ParkingLotManager.ReportApi/Mappings/VehicleMapping.cs create mode 100644 ParkingLotManager.ReportApi/Models/VehicleModel.cs create mode 100644 ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.csproj create mode 100644 ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.http create mode 100644 ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.xml create mode 100644 ParkingLotManager.ReportApi/Program.cs create mode 100644 ParkingLotManager.ReportApi/Properties/launchSettings.json create mode 100644 ParkingLotManager.ReportApi/Services/FlowManagementService.cs create mode 100644 ParkingLotManager.ReportApi/Services/ParkingLotManagerWebApiService.cs create mode 100644 ParkingLotManager.ReportApi/appsettings.Development.json create mode 100644 ParkingLotManager.ReportApi/appsettings.json create mode 100644 ParkingLotManager.WebApi/.gitignore create mode 100644 ParkingLotManager.WebApi/Attributes/ApiKeyAttribute.cs create mode 100644 ParkingLotManager.WebApi/Configuration.cs create mode 100644 ParkingLotManager.WebApi/Controllers/AccountController.cs create mode 100644 ParkingLotManager.WebApi/Controllers/CompanyController.cs create mode 100644 ParkingLotManager.WebApi/Controllers/HomeController.cs create mode 100644 ParkingLotManager.WebApi/Controllers/VehicleController.cs create mode 100644 ParkingLotManager.WebApi/DTOs/CompanyDTO.cs create mode 100644 ParkingLotManager.WebApi/DTOs/Mappings/MappingDTOs.cs create mode 100644 ParkingLotManager.WebApi/DTOs/UserDTO.cs create mode 100644 ParkingLotManager.WebApi/DTOs/VehicleDTO.cs create mode 100644 ParkingLotManager.WebApi/Data/AppDataContext.cs create mode 100644 ParkingLotManager.WebApi/Data/Mappings/CompanyMap.cs create mode 100644 ParkingLotManager.WebApi/Data/Mappings/UserMap.cs create mode 100644 ParkingLotManager.WebApi/Data/Mappings/VehicleMap.cs create mode 100644 ParkingLotManager.WebApi/Enums/EVehicleType.cs create mode 100644 ParkingLotManager.WebApi/Extensions/EntitiesExtensions.cs create mode 100644 ParkingLotManager.WebApi/Extensions/ModelStateExtension.cs create mode 100644 ParkingLotManager.WebApi/Extensions/RoleClaimsExtension.cs create mode 100644 ParkingLotManager.WebApi/Extensions/StringExtension.cs create mode 100644 ParkingLotManager.WebApi/Models/Company.cs create mode 100644 ParkingLotManager.WebApi/Models/Contracts/ICompany.cs create mode 100644 ParkingLotManager.WebApi/Models/Contracts/IUser.cs create mode 100644 ParkingLotManager.WebApi/Models/Contracts/IVehicle.cs create mode 100644 ParkingLotManager.WebApi/Models/Role.cs create mode 100644 ParkingLotManager.WebApi/Models/User.cs create mode 100644 ParkingLotManager.WebApi/Models/Vehicle.cs create mode 100644 ParkingLotManager.WebApi/ParkingLotManager.WebApi.csproj create mode 100644 ParkingLotManager.WebApi/ParkingLotManager.WebApi.http create mode 100644 ParkingLotManager.WebApi/ParkingLotManagerWebApi.xml create mode 100644 ParkingLotManager.WebApi/Program.cs create mode 100644 ParkingLotManager.WebApi/Properties/launchSettings.json create mode 100644 ParkingLotManager.WebApi/Services/Contracts/ITokenService.cs create mode 100644 ParkingLotManager.WebApi/Services/TokenService.cs create mode 100644 ParkingLotManager.WebApi/ValueObjects/Address.cs create mode 100644 ParkingLotManager.WebApi/ValueObjects/Cnpj.cs create mode 100644 ParkingLotManager.WebApi/ValueObjects/ValueObject.cs create mode 100644 ParkingLotManager.WebApi/ViewModels/CompanyViewModels/RegisterCompanyViewModel.cs create mode 100644 ParkingLotManager.WebApi/ViewModels/CompanyViewModels/UpdateCompanyViewModel.cs create mode 100644 ParkingLotManager.WebApi/ViewModels/ResultViewModel.cs create mode 100644 ParkingLotManager.WebApi/ViewModels/UserViewModels/CreateUserViewModel.cs create mode 100644 ParkingLotManager.WebApi/ViewModels/UserViewModels/LoginViewModel.cs create mode 100644 ParkingLotManager.WebApi/ViewModels/UserViewModels/UpdateUserViewModel.cs create mode 100644 ParkingLotManager.WebApi/ViewModels/VehicleViewModels/RegisterVehicleViewModel.cs create mode 100644 ParkingLotManager.WebApi/ViewModels/VehicleViewModels/UpdateVehicleViewModel.cs create mode 100644 ParkingLotManager.WebApi/appsettings.Development.json create mode 100644 ParkingLotManager.WebApi/appsettings.json create mode 100644 ParkingLotManager.XUnitTests/Controllers/AccountControllerTests.cs create mode 100644 ParkingLotManager.XUnitTests/Controllers/CompanyControllerTests.cs create mode 100644 ParkingLotManager.XUnitTests/Controllers/VehicleControllerTests.cs create mode 100644 ParkingLotManager.XUnitTests/Entities/CompanyTests.cs create mode 100644 ParkingLotManager.XUnitTests/Entities/UserTests.cs create mode 100644 ParkingLotManager.XUnitTests/Entities/VehicleTests.cs create mode 100644 ParkingLotManager.XUnitTests/ParkingLotManager.XUnitTests.csproj create mode 100644 ParkingLotManager.XUnitTests/Services/FlowManagementServiceTests.cs create mode 100644 ParkingLotManager.XUnitTests/Services/TokenServiceTests.cs create mode 100644 dotnet-test.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5591b0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,485 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env +.vs + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/ParkingLotManager.ReportApi/Configuration.cs b/ParkingLotManager.ReportApi/Configuration.cs new file mode 100644 index 0000000..2abf40f --- /dev/null +++ b/ParkingLotManager.ReportApi/Configuration.cs @@ -0,0 +1,8 @@ +namespace ParkingLotManager.ReportApi; + +public static class Configuration +{ + public const string Uri = "https://localhost:7255/v1/vehicles"; + public const string ApiKeyName = "api_key"; + public const string ApiKey = "parking_oPt4oylWx0X4wfnj"; +} diff --git a/ParkingLotManager.ReportApi/Controller/ReportController.cs b/ParkingLotManager.ReportApi/Controller/ReportController.cs new file mode 100644 index 0000000..913cee5 --- /dev/null +++ b/ParkingLotManager.ReportApi/Controller/ReportController.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Mvc; +using ParkingLotManager.ReportApi.REST; +using ParkingLotManager.ReportApi.Services; + +namespace ParkingLotManager.ReportApi.Controller; + +[ApiController] +public class ReportController : ControllerBase +{ + private readonly FlowManagementService _flowManagementService; + + public ReportController(FlowManagementService flowManagementService) + => _flowManagementService = flowManagementService; + + [HttpGet("v1/report/entered-vehicles")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetEnteredVehicles() + { + var enteredVehicles = await _flowManagementService.CheckInFlowCalc(); + + return new JsonResult(new { message = $"There are {enteredVehicles} vehicles in the parking lot" }); + } + + [HttpGet("v1/report/departured-vehicles")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetDeparturedVehicles() + { + var departuredVehicles = await _flowManagementService.DepartureFlowCalc(); + + return new JsonResult(new { message = $"{departuredVehicles} vehicles have already departured" }); + } + + [HttpGet("v1/report/entered-vehiclesLH")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetEnteredVehiclesLastHour() + { + var vehiclesLastHour = await _flowManagementService.EnteredVehiclesInTheLastHour(); + + return new JsonResult(new { message = $"{vehiclesLastHour} vehicles entered in the last hour" }); + } + + [HttpGet("v1/report/departured-vehiclesLH")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetDeparturedVehiclesLastHour() + { + var vehiclesLastHour = await _flowManagementService.DeparturedVehiclesInTheLastHour(); + + return new JsonResult(new { message = $"{vehiclesLastHour} vehicles departured in the last hour" }); + } +} diff --git a/ParkingLotManager.ReportApi/DTOs/ParkingLotGenericResponseDTO.cs b/ParkingLotManager.ReportApi/DTOs/ParkingLotGenericResponseDTO.cs new file mode 100644 index 0000000..2f7f160 --- /dev/null +++ b/ParkingLotManager.ReportApi/DTOs/ParkingLotGenericResponseDTO.cs @@ -0,0 +1,36 @@ +using System.Dynamic; +using System.Net; + +namespace ParkingLotManager.ReportApi.DTOs; + +public class ParkingLotGenericResponseDto where T : class +{ + public ParkingLotGenericResponseDto() + { + } + + public ParkingLotGenericResponseDto(T data) + { + Data = data; + } + + public ParkingLotGenericResponseDto(T data, List errors) + { + Data = data; + Errors = errors; + } + + public ParkingLotGenericResponseDto(string error) + { + Errors.Add(error); + } + + public ParkingLotGenericResponseDto(List errors) + { + Errors = errors; + } + + public HttpStatusCode StatusCode { get; set; } + public T? Data { get; set; } + public List? Errors { get; set; } = new(); +} diff --git a/ParkingLotManager.ReportApi/DTOs/VehicleModelDto.cs b/ParkingLotManager.ReportApi/DTOs/VehicleModelDto.cs new file mode 100644 index 0000000..d30c16c --- /dev/null +++ b/ParkingLotManager.ReportApi/DTOs/VehicleModelDto.cs @@ -0,0 +1,15 @@ +namespace ParkingLotManager.ReportApi.DTOs; + +public class VehicleModelDto +{ + public string licensePlate { get; set; } + public string brand { get; set; } + public string model { get; set; } + public string color { get; set; } + public int type { get; set; } + public DateTime createdAt { get; set; } + public DateTime lastUpdateDate { get; set; } + public bool isActive { get; set; } + public object company { get; set; } + public string companyName { get; set; } +} diff --git a/ParkingLotManager.ReportApi/Interfaces/IVehicleFlowManagement.cs b/ParkingLotManager.ReportApi/Interfaces/IVehicleFlowManagement.cs new file mode 100644 index 0000000..d6a1c4c --- /dev/null +++ b/ParkingLotManager.ReportApi/Interfaces/IVehicleFlowManagement.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using ParkingLotManager.ReportApi.DTOs; +using ParkingLotManager.ReportApi.Models; + +namespace ParkingLotManager.ReportApi.Interfaces; + +public interface IVehicleFlowManagement +{ + public Task CheckInFlowCalc(); + public Task DepartureFlowCalc(); + public Task EnteredVehiclesInTheLastHour(); + public Task DeparturedVehiclesInTheLastHour(); +} diff --git a/ParkingLotManager.ReportApi/Interfaces/IVehicleQuery.cs b/ParkingLotManager.ReportApi/Interfaces/IVehicleQuery.cs new file mode 100644 index 0000000..1593334 --- /dev/null +++ b/ParkingLotManager.ReportApi/Interfaces/IVehicleQuery.cs @@ -0,0 +1,9 @@ +using ParkingLotManager.ReportApi.DTOs; +using ParkingLotManager.ReportApi.Models; + +namespace ParkingLotManager.ReportApi.Interfaces; + +public interface IVehicleQuery +{ + public Task> GetVehiclesAsync(); +} diff --git a/ParkingLotManager.ReportApi/Mappings/VehicleMapping.cs b/ParkingLotManager.ReportApi/Mappings/VehicleMapping.cs new file mode 100644 index 0000000..d0f8c64 --- /dev/null +++ b/ParkingLotManager.ReportApi/Mappings/VehicleMapping.cs @@ -0,0 +1,15 @@ +using AutoMapper; +using ParkingLotManager.ReportApi.DTOs; +using ParkingLotManager.ReportApi.Models; + +namespace ParkingLotManager.ReportApi.Mappings; + +public class VehicleMapping : Profile +{ + public VehicleMapping() + { + CreateMap(); + + CreateMap(typeof(ParkingLotGenericResponseDto<>), typeof(ParkingLotGenericResponseDto<>)); + } +} diff --git a/ParkingLotManager.ReportApi/Models/VehicleModel.cs b/ParkingLotManager.ReportApi/Models/VehicleModel.cs new file mode 100644 index 0000000..c756d61 --- /dev/null +++ b/ParkingLotManager.ReportApi/Models/VehicleModel.cs @@ -0,0 +1,21 @@ +using AutoMapper.Configuration.Annotations; +using Newtonsoft.Json; +using ParkingLotManager.WebApi.Enums; +using ParkingLotManager.WebApi.Models; +using System.Text.Json.Serialization; + +namespace ParkingLotManager.ReportApi.Models; + +public class VehicleModel +{ + public string licensePlate { get; set; } + public string brand { get; set; } + public string model { get; set; } + public string color { get; set; } + public int type { get; set; } + public DateTime createdAt { get; set; } + public DateTime lastUpdateDate { get; set; } + public bool isActive { get; set; } + public object company { get; set; } + public string companyName { get; set; } +} diff --git a/ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.csproj b/ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.csproj new file mode 100644 index 0000000..b6aa351 --- /dev/null +++ b/ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + True + ParkingLotManager.ReportApi.xml + + + + + + + + + + + + diff --git a/ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.http b/ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.http new file mode 100644 index 0000000..1344a00 --- /dev/null +++ b/ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.http @@ -0,0 +1,6 @@ +@ParkingLotManager.ReportApi_HostAddress = http://localhost:5144 + +GET {{ParkingLotManager.ReportApi_HostAddress}}/ +Accept: application/json + +### diff --git a/ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.xml b/ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.xml new file mode 100644 index 0000000..ba32adc --- /dev/null +++ b/ParkingLotManager.ReportApi/ParkingLotManager.ReportApi.xml @@ -0,0 +1,14 @@ + + + + ParkingLotManager.ReportApi + + + + + Calculates the amount of checked-in vehicles + + Amount of checked-in vehicles + + + diff --git a/ParkingLotManager.ReportApi/Program.cs b/ParkingLotManager.ReportApi/Program.cs new file mode 100644 index 0000000..dbf791e --- /dev/null +++ b/ParkingLotManager.ReportApi/Program.cs @@ -0,0 +1,72 @@ +using Microsoft.OpenApi.Models; +using ParkingLotManager.ReportApi.Interfaces; +using ParkingLotManager.ReportApi.Mappings; +using ParkingLotManager.ReportApi.Models; +using ParkingLotManager.ReportApi.REST; +using ParkingLotManager.ReportApi.Services; + +var builder = WebApplication.CreateBuilder(args); + +ConfigureMvc(builder); +ConfigureSwagger(builder); +ConfigureMappings(builder); + +//builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +//app.UseAuthorization(); +app.MapControllers(); + +app.Run(); + +static void ConfigureMvc(WebApplicationBuilder builder) +{ + builder.Services.AddControllers().ConfigureApiBehaviorOptions(x => + { + x.SuppressModelStateInvalidFilter = true; + }); +} +static void ConfigureSwagger(WebApplicationBuilder builder) +{ + var linkedin = "https://www.linkedin.com/in/matheusarb/"; + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(x => + { + x.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Parking Lot Report API", + Description = "A Report Api which generates specific reports with the given request", + Contact = new OpenApiContact + { + Name = "Matheus Ribeiro", + Email = "mat.araujoribeiro@gmail.com", + Url = new Uri(linkedin) + }, + License = new OpenApiLicense + { + Name = "Mit License" + }, + Version = "v1" + }); + + var xmlFile = "ParkingLotManager.ReportApi.xml"; + var xmlPath = Path.Combine(Directory.GetCurrentDirectory(), xmlFile); + x.IncludeXmlComments(xmlPath); + }); + +} +static void ConfigureMappings(WebApplicationBuilder builder) +{ + builder.Services.AddAutoMapper(typeof(VehicleMapping)); +} diff --git a/ParkingLotManager.ReportApi/Properties/launchSettings.json b/ParkingLotManager.ReportApi/Properties/launchSettings.json new file mode 100644 index 0000000..9dc4135 --- /dev/null +++ b/ParkingLotManager.ReportApi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:60636", + "sslPort": 44385 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5144", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7010;http://localhost:5144", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ParkingLotManager.ReportApi/Services/FlowManagementService.cs b/ParkingLotManager.ReportApi/Services/FlowManagementService.cs new file mode 100644 index 0000000..e0b1a62 --- /dev/null +++ b/ParkingLotManager.ReportApi/Services/FlowManagementService.cs @@ -0,0 +1,106 @@ +using AutoMapper; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ParkingLotManager.ReportApi.DTOs; +using ParkingLotManager.ReportApi.Interfaces; + +using ParkingLotManager.ReportApi.Models; +using ParkingLotManager.ReportApi.REST; +using System.Dynamic; +using System.Text.Json; + +namespace ParkingLotManager.ReportApi.Services; + +public class FlowManagementService : IVehicleFlowManagement +{ + private readonly IMapper _mapper; + private readonly ParkingLotManagerWebApiService _parkingLotApi; + + protected FlowManagementService() + { + } + + public FlowManagementService(IMapper mapper, ParkingLotManagerWebApiService parkingLotApi) + { + _mapper = mapper; + _parkingLotApi = parkingLotApi; + } + + /// + /// Calculates the amount of checked-in vehicles + /// + /// Amount of checked-in vehicles + public virtual async Task CheckInFlowCalc() + { + var vehicles = await _parkingLotApi.GetVehiclesAsync(); + if (vehicles == null) + return 0; + + var vehiclesDto = _mapper.Map>(vehicles); + + int enteredVehicles = 0; + foreach (var vehicle in vehiclesDto) + { + if (vehicle.isActive) + enteredVehicles++; + } + + return enteredVehicles; + } + + public async Task DepartureFlowCalc() + { + var vehicles = await _parkingLotApi.GetVehiclesAsync(); + if (vehicles == null) + throw new Exception("No vehicles were found"); + + var vehicleDto = _mapper.Map>(vehicles); + + var departuredVehicles = 0; + foreach (var vehicle in vehicleDto) + { + if (vehicle.isActive == false) + departuredVehicles++; + } + + return departuredVehicles; + } + + public async Task EnteredVehiclesInTheLastHour() + { + var vehicles = await _parkingLotApi.GetVehiclesAsync(); + if (vehicles == null) + throw new Exception("No vehicles were found"); + + var count = 0; + var lastHour = DateTime.UtcNow; + foreach (var vehicle in vehicles) + { + var exitHour = DateTime.UtcNow.AddHours(-1); + if (vehicle.createdAt >= lastHour) + count++; + if (vehicle.lastUpdateDate >= exitHour || vehicle.isActive == true) + count++; + } + + return count; + } + + public async Task DeparturedVehiclesInTheLastHour() + { + var vehicles = await _parkingLotApi.GetVehiclesAsync(); + if (vehicles == null) + throw new Exception("No vehicles were found"); + + var vehiclesDepartured = 0; + var lastHour = DateTime.UtcNow.AddHours(-1); + foreach (var vehicle in vehicles) + { + if (vehicle.isActive == false && vehicle.lastUpdateDate >= lastHour) + vehiclesDepartured++; + } + + return vehiclesDepartured; + } +} diff --git a/ParkingLotManager.ReportApi/Services/ParkingLotManagerWebApiService.cs b/ParkingLotManager.ReportApi/Services/ParkingLotManagerWebApiService.cs new file mode 100644 index 0000000..87b1212 --- /dev/null +++ b/ParkingLotManager.ReportApi/Services/ParkingLotManagerWebApiService.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json; +using ParkingLotManager.ReportApi.DTOs; +using ParkingLotManager.ReportApi.Interfaces; +using ParkingLotManager.ReportApi.Models; +using System.Dynamic; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; + +namespace ParkingLotManager.ReportApi.REST; + +public class ParkingLotManagerWebApiService : IVehicleQuery +{ + private readonly HttpClient _httpClient; + private const string Uri = Configuration.Uri; + private const string ApiKeyName = Configuration.ApiKeyName; + private const string ApiKey = Configuration.ApiKey; + + + public ParkingLotManagerWebApiService() + { + _httpClient = new HttpClient(); + } + + public async Task> GetVehiclesAsync() + { + var uri = new StringBuilder(); + uri.Append(Uri + $"/?{ApiKeyName}={ApiKey}"); + + var request = new HttpRequestMessage(HttpMethod.Get, uri.ToString()); + var response = new List(); + + var responseParkingLotWebApi = await _httpClient.SendAsync(request); + var contentResponse = await responseParkingLotWebApi.Content.ReadAsStringAsync(); + var objectResponse = JsonConvert.DeserializeObject>(contentResponse); + + if (!responseParkingLotWebApi.IsSuccessStatusCode) + throw new Exception(responseParkingLotWebApi.StatusCode.ToString()); + else + response = objectResponse; + + return response; + } +} diff --git a/ParkingLotManager.ReportApi/appsettings.Development.json b/ParkingLotManager.ReportApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/ParkingLotManager.ReportApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ParkingLotManager.ReportApi/appsettings.json b/ParkingLotManager.ReportApi/appsettings.json new file mode 100644 index 0000000..ec04bc1 --- /dev/null +++ b/ParkingLotManager.ReportApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/ParkingLotManager.WebApi/.gitignore b/ParkingLotManager.WebApi/.gitignore new file mode 100644 index 0000000..243449b --- /dev/null +++ b/ParkingLotManager.WebApi/.gitignore @@ -0,0 +1,489 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# Data Folder +./Data +./Data/Migrations +./Notes.txt + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/ParkingLotManager.WebApi/Attributes/ApiKeyAttribute.cs b/ParkingLotManager.WebApi/Attributes/ApiKeyAttribute.cs new file mode 100644 index 0000000..c492509 --- /dev/null +++ b/ParkingLotManager.WebApi/Attributes/ApiKeyAttribute.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace ParkingLotManager.WebApi.Attributes; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class ApiKeyAttribute : Attribute, IAsyncActionFilter +{ + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (IsAllowAnonymous(context)) + return; + + if(!context.HttpContext.Request.Query.TryGetValue(Configuration.ApiKeyName, out var extractedApiKey)) + { + context.Result = new ContentResult + { + StatusCode = 401, + Content = "ApiKey not found" + }; + return; + } + + if(!Configuration.ApiKey.Equals(extractedApiKey)) + { + context.Result = new ContentResult + { + StatusCode = 403, + Content = "Access denied" + }; + return; + } + + await next(); + } + + private bool IsAllowAnonymous(ActionExecutingContext context) + { + var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + if (actionDescriptor != null) + { + var allowAnonymousAttribute = actionDescriptor.MethodInfo + .GetCustomAttributes(typeof(AllowAnonymousAttribute), true) + .FirstOrDefault() as AllowAnonymousAttribute; + + return allowAnonymousAttribute != null; + } + + return false; + } +} diff --git a/ParkingLotManager.WebApi/Configuration.cs b/ParkingLotManager.WebApi/Configuration.cs new file mode 100644 index 0000000..b44ec02 --- /dev/null +++ b/ParkingLotManager.WebApi/Configuration.cs @@ -0,0 +1,20 @@ +namespace ParkingLotManager.WebApi; + +public static class Configuration +{ + public static string JwtKey = "IxN9fUjnX0OcZfUl3W44ew==!!!!===="; + + //public static string ApiKeyName = "api_key"; + public const string ApiKeyName = "api_key"; + //public static string ApiKey = "parking_oPt4oylWx0X4wfnj"; + public const string ApiKey = "parking_oPt4oylWx0X4wfnj"; + public static SmtpConfiguration Smtp = new(); + + public class SmtpConfiguration + { + public string Host { get; set; } + public int Port { get; set; } + public string Name { get; set; } + public string Password { get; set; } + } +} diff --git a/ParkingLotManager.WebApi/Controllers/AccountController.cs b/ParkingLotManager.WebApi/Controllers/AccountController.cs new file mode 100644 index 0000000..599595d --- /dev/null +++ b/ParkingLotManager.WebApi/Controllers/AccountController.cs @@ -0,0 +1,315 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using ParkingLotManager.WebApi.Attributes; +using ParkingLotManager.WebApi.Data; +using ParkingLotManager.WebApi.DTOs; +using ParkingLotManager.WebApi.Extensions; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.Services; +using ParkingLotManager.WebApi.ViewModels; +using ParkingLotManager.WebApi.ViewModels.CompanyViewModels; +using ParkingLotManager.WebApi.ViewModels.UserViewModels; +using SecureIdentity.Password; +using System.Data.Common; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace ParkingLotManager.WebApi.Controllers; + + +[ApiController] +public class AccountController : ControllerBase +{ + private readonly AppDataContext _ctx; + private readonly IMapper _mapper; + + protected AccountController() + { + } + + public AccountController(AppDataContext ctx, IMapper mapper) + { + _ctx = ctx; + _mapper = mapper; + } + + /// + /// Log into the system and generate Bearer Token + /// + /// email and password + /// Bearer Token generator + /// Bearer Token + [HttpPost("v1/accounts/login")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task Login( + [FromBody] LoginViewModel viewModel, + [FromServices] TokenService tokenService) + { + if (!ModelState.IsValid) + return BadRequest(new ResultViewModel(ModelState.GetErrors())); + try + { + var user = await _ctx + .Users + .AsNoTracking() + .Include(x => x.Roles) + .FirstOrDefaultAsync(x => x.Email == viewModel.Email); + + if (user == null) + return StatusCode(401, new ResultViewModel("06EX6000 - Invalid user or password")); + if (!PasswordHasher.Verify(user.PasswordHash, viewModel.Password)) + return StatusCode(401, new ResultViewModel("06EX6000 - Invalid user or password")); + + try + { + // Send token + var token = tokenService.GenerateToken(user); + return Ok(new JsonResult(token).Value); + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel>("06EX6001 - Internal server error")); + } + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel>("06EX6001 - Internal server error")); + } + } + + /// + /// Get collection of users + /// + /// API key + /// collection of users + [HttpGet("v1/accounts")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task GetAsync([FromQuery(Name = Configuration.ApiKeyName)] string apiKeyName) + { + try + { + var users = await _ctx.Users.AsNoTracking().ToListAsync(); + if (users == null) + return BadRequest(new ResultViewModel>("06EX6002 - Request could not be processed. Please try another time")); + + var userDto = _mapper.Map>(users); + + + return new JsonResult (userDto); + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel>("06EX6003 - Internal server error")); + } + } + + /// + /// Get user by id + /// + /// user id + /// API key + /// user + [HttpGet("v1/accounts/{id:int}")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task GetByIdAsync( + [FromRoute] int id, + [FromQuery(Name = Configuration.ApiKeyName)] string apiKeyName) + { + try + { + var user = await _ctx.Users.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); + if (user == null) + return BadRequest(new ResultViewModel("06EX6004 - User not found")); + + var userDto = _mapper.Map(user).Display(); + + return new JsonResult(userDto); + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel("06EX6005 - Internal server error")); + } + } + + /// + /// Create a user with no role + /// + /// viewModel to create user + /// API key + /// created user and its Uri + [HttpPost("v1/accounts")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task CreateAsync( + [FromBody] CreateUserViewModel viewModel, + [FromQuery(Name = Configuration.ApiKeyName)] string apiKeyName) + { + if (!ModelState.IsValid) + return BadRequest(new ResultViewModel(ModelState.GetErrors())); + try + { + var user = new User(); + var password = PasswordGenerator.Generate(25); + var createdUser = user.Create(viewModel, password); + var createdUserDto = _mapper.Map(createdUser); + + await _ctx.Users.AddAsync(createdUser); + await _ctx.SaveChangesAsync(); + + return Created($"v1/users/{user.Id}", new ResultViewModel(new + { + createdUserDto.Id, + createdUserDto.Email, + password + })); + } + catch (DbException) + { + return StatusCode(400, new ResultViewModel>("06EX6006 - Email is already in use")); + + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel>("06EX5007 - Internal server error")); + } + } + + /// + /// Update a user by its id + /// + /// viewModel to update user + /// user id + /// API key + /// updated user + [HttpPut("v1/accounts/{id:int}")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task Update( + [FromRoute] int id, + [FromBody] UpdateUserViewModel viewModel, + [FromQuery(Name = Configuration.ApiKeyName)] string apiKeyName) + { + if (!ModelState.IsValid) + return BadRequest(new ResultViewModel(ModelState.GetErrors())); + + try + { + var user = await _ctx.Users.FirstOrDefaultAsync(x => x.Id == id); + if (user == null) + return BadRequest(new ResultViewModel("06EX6004 - Request could not be processed. Please try another time")); + + user.Update(viewModel); + var userDto = _mapper.Map(user).Display(); + + _ctx.Update(user); + await _ctx.SaveChangesAsync(); + + return Ok(new JsonResult(userDto)); + } + catch (DbException) + { + return StatusCode(500, new ResultViewModel("06EX6006 - Request could not be processed. Please try another time")); + } + catch + { + return StatusCode(500, new ResultViewModel("06EX6007 - Internal server error")); + } + } + + /// + /// Delete a user by its id + /// + /// user id + /// API key + /// deleted user + [HttpDelete("v1/accounts/{id:int}")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task Delete( + [FromRoute] int id, + [FromQuery(Name = Configuration.ApiKeyName)] string apiKeyName) + { + try + { + var user = await _ctx.Users.FirstOrDefaultAsync(x => x.Id == id); + if (user == null) + return BadRequest(new ResultViewModel("06EX6008 - Request could not be processed. Please try another time")); + + var userDto = _mapper.Map(user).Display(); + + _ctx.Remove(user); + await _ctx.SaveChangesAsync(); + + return Ok(new JsonResult(userDto)); + } + catch (DbException) + { + return StatusCode(500, new ResultViewModel("06EX6009 - Request could not be processed. Please try another time")); + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel("06EX6010 - Internal server error")); + } + } + + /// + /// Create a user with admin role + /// + /// viewModel to create admin + /// API key + /// user with admin role + [HttpPost("v1/accounts/admin")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task CreateAdminAsync([FromBody] CreateUserViewModel viewModel) + { + if (!ModelState.IsValid) + return BadRequest(new ResultViewModel(ModelState.GetErrors())); + try + { + var user = new User(); + var password = PasswordGenerator.Generate(25); + user.CreateAdmin(viewModel, password); + var userRole = user.Roles?.Select(x => x.Name); + var createdAdminDto = _mapper.Map(user); + + await _ctx.Users.AddAsync(user); + await _ctx.SaveChangesAsync(); + + return Created($"v1/users/admin/{createdAdminDto.Id}", new ResultViewModel(new + { + createdAdminDto.Id, + createdAdminDto.Email, + password, + userRole + })); + } + catch (DbException) + { + return StatusCode(400, new ResultViewModel>("06EX6006 - Email is already in use")); + + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel>("06EX5007 - Internal server error")); + } + } +} diff --git a/ParkingLotManager.WebApi/Controllers/CompanyController.cs b/ParkingLotManager.WebApi/Controllers/CompanyController.cs new file mode 100644 index 0000000..b352533 --- /dev/null +++ b/ParkingLotManager.WebApi/Controllers/CompanyController.cs @@ -0,0 +1,236 @@ +using AutoMapper; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using ParkingLotManager.WebApi.Attributes; +using ParkingLotManager.WebApi.Data; +using ParkingLotManager.WebApi.DTOs; +using ParkingLotManager.WebApi.Extensions; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.ViewModels; +using ParkingLotManager.WebApi.ViewModels.CompanyViewModels; +using ParkingLotManager.WebApi.ViewModels.VehicleViewModels; +using System.Data.Common; + +namespace ParkingLotManager.WebApi.Controllers; + +[ApiController] +[ApiKey] +public class CompanyController : ControllerBase +{ + private readonly AppDataContext _ctx; + private readonly IMapper _mapper; + private const string apiKeyName = Configuration.ApiKeyName; + + protected CompanyController() + { + } + + public CompanyController(AppDataContext ctx, IMapper mapper) + { + _ctx = ctx; + _mapper = mapper; + } + + + /// + /// Get collection of registered companies + /// + /// registered companies data + /// Success + /// Unauthorized + /// Not Found + /// Internal Server Error + [HttpGet("v1/companies/")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task GetAsync([FromQuery(Name = apiKeyName)] string apiKeyName) + { + try + { + var companies = await _ctx.Companies.AsNoTracking().ToListAsync(); + if (companies == null) + return BadRequest(new { message = "05EX5000 - Request could not be processed. Please try another time" }); + + var companiesDto = _mapper.Map>(companies); + + return Ok(new JsonResult(companiesDto)); + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel>("05EX5001 - Internal server error")); + } + } + + /// + /// Get company by name + /// + /// company name + /// API Key + /// company data + /// Success + /// Unauthorized + /// Not Found + /// Internal Server Error + [HttpGet("v1/companies/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task GetAsyncByName( + [FromRoute] string name, + [FromQuery(Name = apiKeyName)] string apiKeyName) + { + try + { + var company = await _ctx.Companies.AsNoTracking().FirstOrDefaultAsync(x => x.Name == name); + if (company == null) + return NotFound(new { message = "05EX5002 - Company not found." }); + + var companyDto = _mapper.Map(company).Display(); + + return Ok(new JsonResult(companyDto)); + } + catch + { + return StatusCode(500, new ResultViewModel("05EX5003 - Internal server error")); + } + } + + /// + /// Register a new company + /// + /// + /// {"name":"string","cnpj":{"cnpjNumber":"string"},"address":{"street":"string","city":"string","zipCode":"string"},"telephone":"string","carSlots":0,"motorcycleSlots":0} + /// + /// company ViewModel + /// API Key + /// data from the new company + /// Created + /// Bad Request + /// Unauthorized + /// Internal Server Error + [HttpPost("v1/companies")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task RegisterAsync( + [FromBody] RegisterCompanyViewModel viewModel, + [FromQuery(Name = apiKeyName)] string apiKeyName) + { + if (!ModelState.IsValid) + return BadRequest(new ResultViewModel(ModelState.GetErrors())); + try + { + var company = new Company(); + company.Create(viewModel); + var companyDto = _mapper.Map(company).Display(); + + await _ctx.Companies.AddAsync(company); + await _ctx.SaveChangesAsync(); + + return Created($"v1/companies/{company.Name}", new JsonResult(companyDto)); + } + catch (DbException) + { + return StatusCode(500, new ResultViewModel>("05EX5002 - Could not register company")); + + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel>("05EX5003 - Internal server error")); + } + } + + /// + /// Update a company + /// + /// + /// {"name":"string","cnpj":{"cnpjNumber":"string"},"address":{"street":"string","city":"string","zipCode":"string"},"telephone":"string","carSlots":0,"motorcycleSlots":0} + /// + /// company name + /// company UpdateViewModel + /// API key + /// company and its updated data + /// Success + /// Bad Request + /// Not Found + /// Internal Server Error + [HttpPut("v1/companies/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task Update( + [FromRoute] string name, + [FromBody] UpdateCompanyViewModel viewModel, + [FromQuery(Name = apiKeyName)] string apiKeyName) + { + if (!ModelState.IsValid) + return BadRequest(new ResultViewModel(ModelState.GetErrors())); + + try + { + var company = await _ctx.Companies.FirstOrDefaultAsync(x => x.Name == name); + if (company == null) + return NotFound(new ResultViewModel("05EX5007 - Company not found")); + + company.Update(viewModel, viewModel.Address); + var companyDto = _mapper.Map(company).Display(); + + _ctx.Update(company); + await _ctx.SaveChangesAsync(); + + return Ok(new JsonResult(companyDto)); + } + catch (DbException) + { + return StatusCode(500, new ResultViewModel>("05EX5007 - Could not update company")); + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel>("05EX5008 - Internal server error")); + } + } + + /// + /// Delete a company + /// + /// company name + /// API key + /// deleted company + /// Success + /// Unauthorized + /// Not Found + /// Internal Server Error + [HttpDelete("v1/companies/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task Delete( + [FromRoute] string name, + [FromQuery(Name = apiKeyName)] string apiKeyName) + { + try + { + var company = await _ctx.Companies.FirstOrDefaultAsync(x => x.Name == name); + if(company == null) + return BadRequest(new ResultViewModel("05EX5005 - Company not found")); + + var companyDto = _mapper.Map(company).Display(); + + _ctx.Remove(company); + await _ctx.SaveChangesAsync(); + + return Ok(new JsonResult(companyDto)); + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel>("05EX5006 - Internal server error")); + } + } +} diff --git a/ParkingLotManager.WebApi/Controllers/HomeController.cs b/ParkingLotManager.WebApi/Controllers/HomeController.cs new file mode 100644 index 0000000..e213509 --- /dev/null +++ b/ParkingLotManager.WebApi/Controllers/HomeController.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ParkingLotManager.WebApi.Attributes; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.ViewModels; +using System.Text.Json.Serialization; + +namespace ParkingLotManager.WebApi.Controllers; + +[ApiController] +public class HomeController : ControllerBase +{ + /// + /// Check API status + /// + /// API status + /// Ok + /// API Offline + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Route("home/status-check")] + public IActionResult CheckStatus() + { + try + { + return Ok(new { message = "API is online" }); + } + catch (Exception) + { + return StatusCode(500, new { message = "00EX0000 - API offline" }); + } + } + + /// + /// Validate API Key + /// + /// + [HttpGet("home/validate-api-key")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult ValidateApiKey() + { + return Ok(new ResultViewModel("Valid ApiKey", null)); + } +} diff --git a/ParkingLotManager.WebApi/Controllers/VehicleController.cs b/ParkingLotManager.WebApi/Controllers/VehicleController.cs new file mode 100644 index 0000000..8610afd --- /dev/null +++ b/ParkingLotManager.WebApi/Controllers/VehicleController.cs @@ -0,0 +1,344 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.EntityFrameworkCore; +using ParkingLotManager.WebApi.Attributes; +using ParkingLotManager.WebApi.Data; +using ParkingLotManager.WebApi.DTOs; +using ParkingLotManager.WebApi.Enums; +using ParkingLotManager.WebApi.Extensions; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.ViewModels; +using ParkingLotManager.WebApi.ViewModels.VehicleViewModels; +using System.Data.Common; + +namespace ParkingLotManager.WebApi.Controllers; + +[ApiController] +public class VehicleController : ControllerBase +{ + private readonly AppDataContext _ctx; + private readonly IMapper _mapper; + private const string apiKeyName = Configuration.ApiKeyName; + + protected VehicleController() + { + } + + public VehicleController(AppDataContext ctx, IMapper mapper) + { + _mapper = mapper; + _ctx = ctx; + } + + /// + /// Get collection of vehicles + /// + /// collection of vehicles + /// Success + /// Unauthorized + /// Not Found + /// Internal Server Error + [HttpGet("v1/vehicles")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task GetAsync([FromQuery(Name = apiKeyName)] string apiKeyName) + { + try + { + var vehicles = await _ctx.Vehicles.AsNoTracking().ToListAsync(); + if (vehicles == null) + return BadRequest(new { message = "01EX1000 - Request could not be processed. Please try another time" }); + + var resultList = vehicles.VehiclesToDtoList(vehicles, _mapper); + + return new JsonResult(resultList); + } + catch + { + return StatusCode(500, new ResultViewModel>("01EX1001 - Internal server error")); + } + } + + /// + /// Get vehicle by licensePlate + /// + /// vehicle data by licensePlate + /// Success + /// Unauthorized + /// Not Found + /// Internal Server Error + [HttpGet("v1/vehicles/{licensePlate}")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task GetByLicensePlateAsync( + [FromRoute] string licensePlate, + [FromQuery(Name = apiKeyName)] string apiKeyName) + { + try + { + var vehicle = await _ctx.Vehicles.AsNoTracking().FirstOrDefaultAsync(x => x.LicensePlate == licensePlate); + if (vehicle is null) + return NotFound(new { message = "01EX1002 - License plate not found." }); + + var vehicleMapping = _mapper.Map(vehicle); + var vehicleDto = vehicleMapping.Display(); + + return Ok(new ResultViewModel(vehicleDto)); + } + catch + { + return StatusCode(500, new ResultViewModel("01EX1003 - Internal server error")); + } + } + + /// + /// Get collection of Ford vehicles. Only works with Admin privileges + /// + /// collection of Ford vehicles + /// Success + /// Unauthorized + /// Not Found + /// Internal Server Error + [HttpGet("v1/vehicles/ford")] + [Authorize(Roles = "admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task GetIfBrandIsFordAsync() + { + try + { + var fordVehicles = await _ctx.Vehicles.Where(x => x.Brand == "Ford").AsNoTracking().ToListAsync(); + if (fordVehicles == null) + return NotFound(new ResultViewModel("01EX1004 - Request could not be processed. Please try again another time")); + + var resultList = fordVehicles.VehiclesToDtoList(fordVehicles, _mapper); + + return new JsonResult(resultList); + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel("01EX1005 - Internal server error")); + } + } + + /// + /// Register a new vehicle + /// + /// + /// {"company":{"cnpj":{"cnpjNumber":"string"},"address":{"street":"string","city":"string","zipCode":"string"}},"licensePlate":"strings","brand":"string","model":"string","color":"string","type":1,"companyName":"string"} + /// + /// new vehicle data + /// Created + /// Bad Request + /// Unauthorized + /// Internal Server Error + [HttpPost("v1/vehicles")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task RegisterAsync( + [FromBody] RegisterVehicleViewModel viewModel, + [FromQuery(Name = apiKeyName)] string apiKeyName) + { + if (!ModelState.IsValid) + return BadRequest(new ResultViewModel(ModelState.GetErrors())); + try + { + var company = await _ctx.Companies.FirstOrDefaultAsync(x => x.Name == viewModel.CompanyName); + if (company == null) + return new JsonResult(new { message = "Company does not exist" }); + + if (viewModel.Type == EVehicleType.Car) + { + if (company.CarSlots == 0) + return new JsonResult(new { message = "Sorry, car slots are full" }); + } + if (viewModel.Type == EVehicleType.Motorcycle) + { + if (company.MotorcycleSlots == 0) + return new JsonResult(new { message = "Sorry, motorcycle slots are full" }); + } + + var createdVehicle = new Vehicle().Create(viewModel); + var vehicleDto = _mapper.Map(createdVehicle).Display(); + + await _ctx.Vehicles.AddAsync(createdVehicle); + await _ctx.SaveChangesAsync(); + + return Created($"vehicles/v1/{createdVehicle.LicensePlate}", new JsonResult(vehicleDto)); + } + catch (DbUpdateException) + { + return StatusCode(500, new ResultViewModel("01EX2000 - Could not register vehicle")); + } + catch (Exception) + { + return StatusCode(500, new ResultViewModel("01EX2001 - Internal server error")); + } + + } + + /// + /// Update data of a registered vehicle + /// + /// updated data of vehicle + /// + /// {"company":{"cnpj":{"cnpjNumber":"string"},"address":{"street":"string","city":"string","zipCode":"string"}},"licensePlate":"strings","brand":"string","model":"string","color":"string","type":1,"companyName":"string"} + /// + /// Success + /// Bad Request + /// Unauthorized + /// Internal Server Error + [HttpPut("v1/vehicles/{licensePlate}")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task Update( + [FromRoute] string licensePlate, + [FromBody] UpdateVehicleViewModel viewModel, + [FromQuery(Name = apiKeyName)] string apiKeyName) + { + if (!ModelState.IsValid) + return BadRequest(new ResultViewModel(ModelState.GetErrors())); + try + { + var vehicle = await _ctx.Vehicles.FirstOrDefaultAsync(x => x.LicensePlate == licensePlate); + if (vehicle == null) + return NotFound(new ResultViewModel("01EX3000 - Vehicle not found.")); + + vehicle.Update(viewModel); + var updatedDto = _mapper.Map(vehicle).Display(); + + _ctx.Update(vehicle); + await _ctx.SaveChangesAsync(); + + return new JsonResult(updatedDto); + } + catch + { + return StatusCode(500, new ResultViewModel("01EX3001 - Internal server error")); + } + } + + /// + /// Delete vehicle by licensePlate + /// + /// data of deleted vehicle + /// Success + /// Unauthorized + /// Not Found + /// Internal Server Error + [HttpDelete("v1/vehicles/{licensePlate}")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task Delete( + [FromRoute] string licensePlate, + [FromQuery(Name = apiKeyName)] string apiKeyName) + { + try + { + var vehicle = await _ctx.Vehicles.FirstOrDefaultAsync(x => x.LicensePlate == licensePlate); + if (vehicle == null) + return NotFound(new ResultViewModel("01EX4000 - Vehicle not found.")); + + var deletedDto = _mapper.Map(vehicle).Display(); + + _ctx.Vehicles.Remove(vehicle); + await _ctx.SaveChangesAsync(); + + return Ok(new JsonResult(deletedDto)); + } + catch (Exception) + { + return StatusCode(500, new { message = "01EX4001 - Internal server error" }); + } + } + + /// + /// Registers a vehicle departure + /// + /// vehicle license plate + /// API key + /// vehicle data + /// Success + /// Unauthorized + /// Not Found + /// Internal Server Error + [HttpPut("v1/vehicles/departure/{licensePlate}")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task DepartureAsync( + [FromRoute] string licensePlate, + [FromQuery(Name = apiKeyName)] string apiKeyName) + { + try + { + var vehicle = await _ctx.Vehicles.FirstOrDefaultAsync(x => x.LicensePlate == licensePlate); + if (vehicle == null) + return NotFound(new ResultViewModel("01EX4001 - Vehicle not found.")); + + vehicle.Departure(); + _ctx.Update(vehicle); + await _ctx.SaveChangesAsync(); + + return Ok(new JsonResult(new { message = "Vehicle has exited" })); + } + catch (Exception) + { + return StatusCode(500, new { message = "01EX4003 - Internal server error" }); + } + } + + /// + /// Reactivate a vehicle which already has been in the parking lot + /// + /// vehicle license plate + /// API key + /// reactivation confirmation + /// Success + /// Unauthorized + /// Not Found + /// Internal Server Error + [HttpPut("v1/vehicles/reentered/{licensePlate}")] + [ApiKey] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public virtual async Task ReenteredAsync( + [FromRoute] string licensePlate, + [FromQuery(Name = apiKeyName)] string apiKeyName) + { + var vehicle = _ctx.Vehicles.FirstOrDefault(x => x.LicensePlate == licensePlate); + if (vehicle == null) + return NotFound(new ResultViewModel("01EX4004 - Vehicle not found.")); + + vehicle.Reentered(); + _ctx.Update(vehicle); + await _ctx.SaveChangesAsync(); + + return new JsonResult(new { isActive = vehicle.IsActive, lastUpdate = vehicle.LastUpdateDate }); + } +} diff --git a/ParkingLotManager.WebApi/DTOs/CompanyDTO.cs b/ParkingLotManager.WebApi/DTOs/CompanyDTO.cs new file mode 100644 index 0000000..6e228ba --- /dev/null +++ b/ParkingLotManager.WebApi/DTOs/CompanyDTO.cs @@ -0,0 +1,32 @@ +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.ValueObjects; + +namespace ParkingLotManager.WebApi.DTOs; + +public class CompanyDTO +{ + public string Name { get; set; } + public Cnpj Cnpj { get; set; } + public Address Address { get; set; } + public string Telephone { get; set; } + public int CarSlots { get; set; } + public int MotorcycleSlots { get; set; } + + public IList? Vehicles { get; set; } + public IList? Users { get; set; } + + public virtual object Display() + { + var companyDto = new + { + Name = this.Name, + Cnpj = this.Cnpj, + Address = this.Address, + Telephone = this.Telephone, + CarSlots = this.CarSlots, + MotorcycleSlots = this.MotorcycleSlots, + }; + + return companyDto; + } +} diff --git a/ParkingLotManager.WebApi/DTOs/Mappings/MappingDTOs.cs b/ParkingLotManager.WebApi/DTOs/Mappings/MappingDTOs.cs new file mode 100644 index 0000000..b9c0da8 --- /dev/null +++ b/ParkingLotManager.WebApi/DTOs/Mappings/MappingDTOs.cs @@ -0,0 +1,14 @@ +using AutoMapper; +using ParkingLotManager.WebApi.Models; + +namespace ParkingLotManager.WebApi.DTOs.Mappings; + +public class MappingDTOs : Profile +{ + public MappingDTOs() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } +} diff --git a/ParkingLotManager.WebApi/DTOs/UserDTO.cs b/ParkingLotManager.WebApi/DTOs/UserDTO.cs new file mode 100644 index 0000000..6648f68 --- /dev/null +++ b/ParkingLotManager.WebApi/DTOs/UserDTO.cs @@ -0,0 +1,51 @@ +using ParkingLotManager.WebApi.Models; + +namespace ParkingLotManager.WebApi.DTOs; + +public class UserDTO +{ + public int Id { get; } + public string Name { get; private set; } + public string Email { get; private set; } + public string PasswordHash { get; private set; } + public string Slug { get; private set; } + + public Company? Company { get; private set; } + public string CompanyName { get; private set; } + + public IList? Roles { get; set; } + + public virtual object Display() + { + var userDto = new + { + Id = Id, + Name = this.Name, + Email = this.Email, + Slug = this.Slug, + CompanyName = this.CompanyName + }; + + return userDto; + } + + public virtual List DisplayList(List list) + { + var result = new List(); + + foreach (var user in list) + { + var userDto = new + { + Id = user.Id, + Name = user.Name, + Email = user.Email, + Slug = user.Slug, + CompanyName = user.CompanyName, + }; + result.Add(userDto); + } + + return result; + } +} diff --git a/ParkingLotManager.WebApi/DTOs/VehicleDTO.cs b/ParkingLotManager.WebApi/DTOs/VehicleDTO.cs new file mode 100644 index 0000000..6c679e4 --- /dev/null +++ b/ParkingLotManager.WebApi/DTOs/VehicleDTO.cs @@ -0,0 +1,56 @@ +using ParkingLotManager.WebApi.Enums; +using ParkingLotManager.WebApi.Models; + +namespace ParkingLotManager.WebApi.DTOs; + +public class VehicleDTO +{ + public string LicensePlate { get; private set; } + public string Brand { get; private set; } + public string Model { get; private set; } + public string Color { get; private set; } + public EVehicleType Type { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime LastUpdateDate { get; private set; } + public bool IsActive { get; private set; } + + public Company? Company { get; private set; } + public string CompanyName { get; private set; } + + public virtual object Display() + { + var vehicleDto = new + { + licensePlate = this.LicensePlate, + brand = this.Brand, + model = this.Model, + color = this.Color, + Type = this.Type, + IsActive = this.IsActive, + CompanyName = this.CompanyName + }; + + return vehicleDto; + } + + public virtual List DisplayList(List list) + { + var result = new List(); + + foreach(var user in list) + { + var vehicleDto = new + { + licensePlate = this.LicensePlate, + brand = this.Brand, + model = this.Model, + color = this.Color, + Type = this.Type, + CompanyName = this.CompanyName + }; + result.Add(vehicleDto); + } + + return result; + } +} diff --git a/ParkingLotManager.WebApi/Data/AppDataContext.cs b/ParkingLotManager.WebApi/Data/AppDataContext.cs new file mode 100644 index 0000000..f21a159 --- /dev/null +++ b/ParkingLotManager.WebApi/Data/AppDataContext.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore; +using ParkingLotManager.WebApi.Data.Mappings; +using ParkingLotManager.WebApi.Enums; +using ParkingLotManager.WebApi.Models; +using System.Diagnostics; + +namespace ParkingLotManager.WebApi.Data; + +public class AppDataContext : DbContext +{ + protected AppDataContext() + { + } + + public AppDataContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Companies { get; set; } + public virtual DbSet Vehicles { get; set; } + public virtual DbSet Users { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new CompanyMap()); + modelBuilder.ApplyConfiguration(new VehicleMap()); + modelBuilder.ApplyConfiguration(new UserMap()); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + var pendingChanges = ChangeTracker.Entries() + .Where(e => e.State == EntityState.Added); + + foreach (var entry in pendingChanges) + { + var vehicle = entry.Entity; + var company = Companies.FirstOrDefault(c => c.Name == vehicle.CompanyName); + + if (company != null) + { + if (vehicle.Type == EVehicleType.Car) + { + company.CarSlots--; + } + else if (vehicle.Type == EVehicleType.Motorcycle) + { + company.MotorcycleSlots--; + } + } + } + + // Salve as alterações no banco de dados + return await base.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/ParkingLotManager.WebApi/Data/Mappings/CompanyMap.cs b/ParkingLotManager.WebApi/Data/Mappings/CompanyMap.cs new file mode 100644 index 0000000..d827ed5 --- /dev/null +++ b/ParkingLotManager.WebApi/Data/Mappings/CompanyMap.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ParkingLotManager.WebApi.Models; + +namespace ParkingLotManager.WebApi.Data.Mappings; + +public class CompanyMap : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + // Table + builder.ToTable("Company"); + + // Primary Key + builder.HasKey(x => x.Name); + + builder.Property(x => x.Name) + .IsRequired() + .HasColumnName("Name") + .HasColumnType("NVARCHAR") + .HasMaxLength(80); + + builder.OwnsOne(x => x.Cnpj, cnpj => + { + cnpj.Property(p => p.CnpjNumber) + .IsRequired() + .HasColumnName("CnpjNumber") + .HasColumnType("NVARCHAR") + .HasMaxLength(14); + }); + + builder.OwnsOne(x => x.Address, address => + { + address.Property(p => p.Street) + .IsRequired() + .HasColumnName("Street") + .HasColumnType("NVARCHAR") + .HasMaxLength(100); + + address.Property(p => p.City) + .IsRequired() + .HasColumnName("City") + .HasColumnType("NVARCHAR") + .HasMaxLength(50); + + address.Property(p => p.ZipCode) + .IsRequired() + .HasColumnName("ZipCode") + .HasColumnType("NVARCHAR") + .HasMaxLength(30); + }); + + builder.Property(x => x.Telephone) + .IsRequired() + .HasColumnName("Telephone") + .HasColumnType("NVARCHAR") + .HasMaxLength(30); + + builder.Property(x => x.CarSlots) + .IsRequired() + .HasColumnName("CarSlots") + .HasColumnType("INT"); + + builder.Property(x => x.MotorcycleSlots) + .IsRequired() + .HasColumnName("MotorcycleSlots") + .HasColumnType("INT"); + } +} \ No newline at end of file diff --git a/ParkingLotManager.WebApi/Data/Mappings/UserMap.cs b/ParkingLotManager.WebApi/Data/Mappings/UserMap.cs new file mode 100644 index 0000000..4803803 --- /dev/null +++ b/ParkingLotManager.WebApi/Data/Mappings/UserMap.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ParkingLotManager.WebApi.Models; + +namespace ParkingLotManager.WebApi.Data.Mappings; + +public class UserMap : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + // Table + builder.ToTable("User"); + + // PK + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .ValueGeneratedOnAdd() + .UseIdentityColumn(); + + builder.Property(x => x.Name) + .IsRequired() + .HasColumnName("Name") + .HasColumnType("NVARCHAR") + .HasMaxLength(80); + + builder.Property(x => x.Email) + .IsRequired() + .HasColumnName("Email") + .HasColumnType("VARCHAR") + .HasMaxLength(160); + + builder.Property(x => x.PasswordHash) + .IsRequired() + .HasColumnName("PasswordHash"); + + builder.Property(x => x.Slug) + .IsRequired() + .HasColumnName("Slug") + .HasColumnType("VARCHAR") + .HasMaxLength(80); + + // Relationships + // 1 to n + builder.HasOne(x => x.Company) + .WithMany(x => x.Users) + .HasForeignKey(x => x.CompanyName) + .HasConstraintName("FK_Users_Company") + .OnDelete(DeleteBehavior.Cascade); + + // n to n + builder + .HasMany(x => x.Roles) + .WithMany(x => x.Users) + .UsingEntity>( + "UserRole", + role => role + .HasOne() + .WithMany() + .HasForeignKey("RoleId") + .HasConstraintName("FK_UserRole_RoleId") + .OnDelete(DeleteBehavior.Cascade), + user => user + .HasOne() + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("FK_UserRole_UserId") + .OnDelete(DeleteBehavior.Cascade)); + } +} diff --git a/ParkingLotManager.WebApi/Data/Mappings/VehicleMap.cs b/ParkingLotManager.WebApi/Data/Mappings/VehicleMap.cs new file mode 100644 index 0000000..a9b8088 --- /dev/null +++ b/ParkingLotManager.WebApi/Data/Mappings/VehicleMap.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ParkingLotManager.WebApi.Models; + +namespace ParkingLotManager.WebApi.Data.Mappings; + +public class VehicleMap : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Vehicle"); + + builder.HasKey(x => x.LicensePlate); + builder.Property(x => x.LicensePlate) + .IsRequired() + .HasColumnName("LicensePlate") + .HasColumnType("NVARCHAR") + .HasMaxLength(8); + + builder.Property(x => x.Brand) + .IsRequired() + .HasColumnName("Brand") + .HasColumnType("NVARCHAR") + .HasMaxLength(80); + + builder.Property(x => x.Model) + .IsRequired() + .HasColumnName("Model") + .HasColumnType("NVARCHAR") + .HasMaxLength(80); + + builder.Property(x => x.Color) + .IsRequired() + .HasColumnName("Color") + .HasColumnType("NVARCHAR") + .HasMaxLength(40); + + builder.Property(x => x.Type) + .IsRequired() + .HasConversion() + .HasColumnName("Type") + .HasColumnType("INT"); + + builder.Property(x => x.CreatedAt) + .IsRequired() + .HasColumnName("CreatedAt") + .HasColumnType("DATETIME") + .HasMaxLength(60) + .ValueGeneratedOnAdd() + .HasDefaultValueSql("GETUTCDATE()"); + + builder.Property(x => x.LastUpdateDate) + .IsRequired() + .HasColumnName("LastUpdateDate") + .HasColumnType("DATETIME") + .HasMaxLength(60) + .HasDefaultValueSql("GETUTCDATE()"); + + builder.Property(x => x.IsActive) + .IsRequired() + .HasColumnName("IsActive") + .HasColumnType("BIT"); + + // Relationships + builder.HasOne(x => x.Company) + .WithMany(x => x.Vehicles) + .HasForeignKey(x => x.CompanyName) + .HasConstraintName("FK_Vehicles_Company") + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/ParkingLotManager.WebApi/Enums/EVehicleType.cs b/ParkingLotManager.WebApi/Enums/EVehicleType.cs new file mode 100644 index 0000000..30d6476 --- /dev/null +++ b/ParkingLotManager.WebApi/Enums/EVehicleType.cs @@ -0,0 +1,7 @@ +namespace ParkingLotManager.WebApi.Enums; + +public enum EVehicleType +{ + Car = 1, + Motorcycle = 2 +} diff --git a/ParkingLotManager.WebApi/Extensions/EntitiesExtensions.cs b/ParkingLotManager.WebApi/Extensions/EntitiesExtensions.cs new file mode 100644 index 0000000..f009ec4 --- /dev/null +++ b/ParkingLotManager.WebApi/Extensions/EntitiesExtensions.cs @@ -0,0 +1,34 @@ +using ParkingLotManager.WebApi.Models; +using AutoMapper; +using ParkingLotManager.WebApi.DTOs; + +namespace ParkingLotManager.WebApi.Extensions; + +public static class EntitiesExtensions +{ + public static List VehiclesToDtoList(this List vehicleList, List vehicles, IMapper mapper) + { + var resultList = new List(); + foreach(var item in vehicles) + { + if (!item.IsActive) + continue; + var vehicleMapping = mapper.Map(item); + var vehicleDto = vehicleMapping.Display(); + resultList.Add(vehicleDto); + } + return resultList; + } + + public static List CompaniesToDtoList(this List companyList, List companies, IMapper mapper) + { + var resultList = new List(); + foreach(var item in companies) + { + var companyMapping = mapper.Map(item); + var companyDto = companyMapping.Display(); + resultList.Add(companyDto); + } + return resultList; + } +} diff --git a/ParkingLotManager.WebApi/Extensions/ModelStateExtension.cs b/ParkingLotManager.WebApi/Extensions/ModelStateExtension.cs new file mode 100644 index 0000000..f7b5264 --- /dev/null +++ b/ParkingLotManager.WebApi/Extensions/ModelStateExtension.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace ParkingLotManager.WebApi.Extensions; + +public static class ModelStateExtension +{ + public static List GetErrors(this ModelStateDictionary modelState) + { + var result = new List(); + foreach (var item in modelState.Values) + result.AddRange(item.Errors.Select(x => x.ErrorMessage)); + return result; + } +} diff --git a/ParkingLotManager.WebApi/Extensions/RoleClaimsExtension.cs b/ParkingLotManager.WebApi/Extensions/RoleClaimsExtension.cs new file mode 100644 index 0000000..585159a --- /dev/null +++ b/ParkingLotManager.WebApi/Extensions/RoleClaimsExtension.cs @@ -0,0 +1,22 @@ +using ParkingLotManager.WebApi.Models; +using System.Collections; +using System.Security.Claims; + +namespace ParkingLotManager.WebApi.Extensions; + +public static class RoleClaimsExtension +{ + public static IEnumerable GetClaims(this User user) + { + var result = new List + { + new Claim(ClaimTypes.Name, user.Email), + }; + + result.AddRange( + user.Roles.Select(role => new Claim(ClaimTypes.Role, role.Slug)) + ); + + return result; + } +} diff --git a/ParkingLotManager.WebApi/Extensions/StringExtension.cs b/ParkingLotManager.WebApi/Extensions/StringExtension.cs new file mode 100644 index 0000000..f621bc3 --- /dev/null +++ b/ParkingLotManager.WebApi/Extensions/StringExtension.cs @@ -0,0 +1,15 @@ +using System.Diagnostics.CodeAnalysis; + +namespace ParkingLotManager.WebApi.Extensions; + +public static class StringExtension +{ + public static bool IsNullOrEmptyOrWhiteSpace([NotNullWhen(false)] this string? value) + { + if (value?.Trim() == "") + return true; + if(string.IsNullOrEmpty(value)) + return true; + return false; + } +} diff --git a/ParkingLotManager.WebApi/Models/Company.cs b/ParkingLotManager.WebApi/Models/Company.cs new file mode 100644 index 0000000..56ec658 --- /dev/null +++ b/ParkingLotManager.WebApi/Models/Company.cs @@ -0,0 +1,50 @@ +using Microsoft.IdentityModel.Tokens; +using ParkingLotManager.WebApi.Extensions; +using ParkingLotManager.WebApi.Models.Contracts; +using ParkingLotManager.WebApi.ValueObjects; +using ParkingLotManager.WebApi.ViewModels.CompanyViewModels; + +namespace ParkingLotManager.WebApi.Models; + +public class Company : ICompany +{ + public string Name { get; private set; } + public Cnpj Cnpj { get; private set; } + public Address Address { get; private set; } + public string Telephone { get; private set; } + public int CarSlots { get; set; } + public int MotorcycleSlots { get; set; } + + public IList? Vehicles { get; private set; } + public IList? Users { get; private set; } + + public virtual Company Create(RegisterCompanyViewModel viewModel) + { + Name = viewModel.Name; + Cnpj = viewModel.Cnpj; + Address = viewModel.Address; + Telephone = viewModel.Telephone; + CarSlots = viewModel.CarSlots == 0 ? 1 : viewModel.CarSlots; + MotorcycleSlots = viewModel.MotorcycleSlots == 0 ? 1 : viewModel.MotorcycleSlots; + + return this; + } + + public virtual Company Update(UpdateCompanyViewModel viewModel, Address address) + { + Name = viewModel.Name.IsNullOrEmptyOrWhiteSpace() ? this.Name : viewModel.Name; + Cnpj = !viewModel.Cnpj.IsValid ? this.Cnpj : viewModel.Cnpj; + + if(this.Address == null ) + { + this.Address = new Address(address.Street, address.City, address.ZipCode); + } + Address = viewModel.Address == null ? Address : this.Address.Update(viewModel.Address); + + Telephone = viewModel.Telephone.IsNullOrEmptyOrWhiteSpace() ? this.Telephone : viewModel.Telephone; + CarSlots = viewModel.CarSlots == 0 || viewModel.CarSlots == null ? this.CarSlots : viewModel.CarSlots; + MotorcycleSlots = viewModel.MotorcycleSlots == 0 || viewModel.MotorcycleSlots == null ? this.MotorcycleSlots : viewModel.MotorcycleSlots; + + return this; + } +} diff --git a/ParkingLotManager.WebApi/Models/Contracts/ICompany.cs b/ParkingLotManager.WebApi/Models/Contracts/ICompany.cs new file mode 100644 index 0000000..67ad885 --- /dev/null +++ b/ParkingLotManager.WebApi/Models/Contracts/ICompany.cs @@ -0,0 +1,11 @@ +using ParkingLotManager.WebApi.ValueObjects; +using ParkingLotManager.WebApi.ViewModels.CompanyViewModels; + +namespace ParkingLotManager.WebApi.Models.Contracts; + +public interface ICompany +{ + public Company Create(RegisterCompanyViewModel viewModel); + + public Company Update(UpdateCompanyViewModel viewModel, Address address); +} diff --git a/ParkingLotManager.WebApi/Models/Contracts/IUser.cs b/ParkingLotManager.WebApi/Models/Contracts/IUser.cs new file mode 100644 index 0000000..963965e --- /dev/null +++ b/ParkingLotManager.WebApi/Models/Contracts/IUser.cs @@ -0,0 +1,12 @@ +using ParkingLotManager.WebApi.ViewModels.UserViewModels; + +namespace ParkingLotManager.WebApi.Models.Contracts; + +public interface IUser +{ + public User Create(CreateUserViewModel viewModel, string password); + + public User Update(UpdateUserViewModel viewModel); + + public User CreateAdmin(CreateUserViewModel viewModel, string password); +} diff --git a/ParkingLotManager.WebApi/Models/Contracts/IVehicle.cs b/ParkingLotManager.WebApi/Models/Contracts/IVehicle.cs new file mode 100644 index 0000000..04ca640 --- /dev/null +++ b/ParkingLotManager.WebApi/Models/Contracts/IVehicle.cs @@ -0,0 +1,10 @@ +using ParkingLotManager.WebApi.ViewModels.VehicleViewModels; + +namespace ParkingLotManager.WebApi.Models.Contracts; + +public interface IVehicle +{ + public Vehicle Create(RegisterVehicleViewModel viewModel); + + public Vehicle Update(UpdateVehicleViewModel viewModel); +} \ No newline at end of file diff --git a/ParkingLotManager.WebApi/Models/Role.cs b/ParkingLotManager.WebApi/Models/Role.cs new file mode 100644 index 0000000..8318580 --- /dev/null +++ b/ParkingLotManager.WebApi/Models/Role.cs @@ -0,0 +1,13 @@ +using ParkingLotManager.WebApi.Models; +using System.Collections.Generic; + +namespace ParkingLotManager.WebApi.Models; + +public class Role +{ + public int Id { get; set; } + public string Name { get; set; } + public string Slug { get; set; } + + public IList Users { get; set; } +} \ No newline at end of file diff --git a/ParkingLotManager.WebApi/Models/User.cs b/ParkingLotManager.WebApi/Models/User.cs new file mode 100644 index 0000000..a435c1d --- /dev/null +++ b/ParkingLotManager.WebApi/Models/User.cs @@ -0,0 +1,55 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.IdentityModel.Tokens; +using ParkingLotManager.WebApi.Extensions; +using ParkingLotManager.WebApi.Models.Contracts; +using ParkingLotManager.WebApi.ViewModels.UserViewModels; +using SecureIdentity.Password; +using System.Xml; + +namespace ParkingLotManager.WebApi.Models; + +public class User : IUser +{ + public int Id { get; set; } + public string Name { get; private set; } + public string Email { get; private set; } + public string PasswordHash { get; private set; } + public string Slug { get; private set; } + + public Company? Company { get; private set; } + public string CompanyName { get; private set; } + + public IList? Roles { get; set; } + + public virtual User Create(CreateUserViewModel viewModel, string password) + { + Name = viewModel.Name; + Email = viewModel.Email; + PasswordHash = PasswordHasher.Hash(password); + Slug = viewModel.Email.Replace("@", "-").Replace(".", "-"); + CompanyName = viewModel.CompanyName; + + return this; + } + + public virtual User Update(UpdateUserViewModel viewModel) + { + Name = viewModel.Name.IsNullOrEmptyOrWhiteSpace() ? Name : viewModel.Name; + Email = viewModel.Email.IsNullOrEmptyOrWhiteSpace() ? Email : viewModel.Email; + PasswordHash = viewModel.PasswordHash.IsNullOrEmptyOrWhiteSpace() ? PasswordHash : viewModel.PasswordHash; + + return this; + } + + public virtual User CreateAdmin(CreateUserViewModel viewModel, string password) + { + Name = viewModel.Name; + Email = viewModel.Email; + PasswordHash = PasswordHasher.Hash(password); + Roles = new List { new Role() { Name = "admin", Slug = "admin" } }; + Slug = viewModel.Email.Replace("@", "-").Replace(".", "-"); + CompanyName = viewModel.CompanyName; + + return this; + } +} \ No newline at end of file diff --git a/ParkingLotManager.WebApi/Models/Vehicle.cs b/ParkingLotManager.WebApi/Models/Vehicle.cs new file mode 100644 index 0000000..95b3db8 --- /dev/null +++ b/ParkingLotManager.WebApi/Models/Vehicle.cs @@ -0,0 +1,69 @@ +using Microsoft.IdentityModel.Tokens; +using ParkingLotManager.WebApi.DTOs; +using ParkingLotManager.WebApi.Enums; +using ParkingLotManager.WebApi.Extensions; +using ParkingLotManager.WebApi.Models.Contracts; +using ParkingLotManager.WebApi.ViewModels.VehicleViewModels; + +namespace ParkingLotManager.WebApi.Models; + +public class Vehicle : IVehicle +{ + public string LicensePlate { get; set; } + public string Brand { get; set; } + public string Model { get; set; } + public string Color { get; set; } + public EVehicleType Type { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime LastUpdateDate { get; set; } + public bool IsActive { get; set; } + + public Company? Company { get; set; } + public string CompanyName { get; set; } + + public virtual Vehicle Create(RegisterVehicleViewModel viewModel) + { + LicensePlate = viewModel.LicensePlate; + Model = viewModel.Model; + Brand = viewModel.Brand; + Color = viewModel.Color; + Type = viewModel.Type; + CompanyName = viewModel.CompanyName; + IsActive = true; + + return this; + } + + public virtual Vehicle Update(UpdateVehicleViewModel viewModel) + { + LicensePlate = viewModel.LicensePlate.IsNullOrEmptyOrWhiteSpace() ? LicensePlate : viewModel.LicensePlate; + Brand = viewModel.Brand.IsNullOrEmptyOrWhiteSpace() ? Brand : viewModel.Brand; + Model = viewModel.Model.IsNullOrEmptyOrWhiteSpace() ? Model : viewModel.Model; + Color = viewModel.Color.IsNullOrEmptyOrWhiteSpace() ? Color : viewModel.Color; + Type = viewModel.Type != Type ? viewModel.Type : Type; + CompanyName = viewModel.CompanyName.IsNullOrEmptyOrWhiteSpace() ? CompanyName : viewModel.CompanyName; + + return this; + } + + public virtual bool Departure() + { + this.IsActive = false; + this.LastUpdateDate = DateTime.UtcNow; + return true; + } + + public virtual bool Reentered() + { + this.IsActive = true; + this.LastUpdateDate = DateTime.UtcNow; + return true; + } + + static void ChangeLicensePlate(string licensePlate, Vehicle vehicle) + { + if (licensePlate.IsNullOrEmptyOrWhiteSpace()) + return; + vehicle.LicensePlate = licensePlate; + } +} diff --git a/ParkingLotManager.WebApi/ParkingLotManager.WebApi.csproj b/ParkingLotManager.WebApi/ParkingLotManager.WebApi.csproj new file mode 100644 index 0000000..b76d12e --- /dev/null +++ b/ParkingLotManager.WebApi/ParkingLotManager.WebApi.csproj @@ -0,0 +1,38 @@ + + + + net8.0 + enable + enable + True + ParkingLotManagerWebApi.xml + + + + 1701;1702;1591 + + + + 1701;1702;1591 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/ParkingLotManager.WebApi/ParkingLotManager.WebApi.http b/ParkingLotManager.WebApi/ParkingLotManager.WebApi.http new file mode 100644 index 0000000..c11d161 --- /dev/null +++ b/ParkingLotManager.WebApi/ParkingLotManager.WebApi.http @@ -0,0 +1,6 @@ +@ParkingLotManager.WebApi_HostAddress = http://localhost:5166 + +GET {{ParkingLotManager.WebApi_HostAddress}}/home/ +Accept: application/json + +### diff --git a/ParkingLotManager.WebApi/ParkingLotManagerWebApi.xml b/ParkingLotManager.WebApi/ParkingLotManagerWebApi.xml new file mode 100644 index 0000000..ce07c50 --- /dev/null +++ b/ParkingLotManager.WebApi/ParkingLotManagerWebApi.xml @@ -0,0 +1,233 @@ + + + + ParkingLotManager.WebApi + + + + + Log into the system and generate Bearer Token + + email and password + Bearer Token generator + Bearer Token + + + + Get collection of users + + API key + collection of users + + + + Get user by id + + user id + API key + user + + + + Create a user with no role + + viewModel to create user + API key + created user and its Uri + + + + Update a user by its id + + viewModel to update user + user id + API key + updated user + + + + Delete a user by its id + + user id + API key + deleted user + + + + Create a user with admin role + + viewModel to create admin + API key + user with admin role + + + + Get collection of registered companies + + registered companies data + Success + Unauthorized + Not Found + Internal Server Error + + + + Get company by name + + company name + API Key + company data + Success + Unauthorized + Not Found + Internal Server Error + + + + Register a new company + + + {"name":"string","cnpj":{"cnpjNumber":"string"},"address":{"street":"string","city":"string","zipCode":"string"},"telephone":"string","carSlots":0,"motorcycleSlots":0} + + company ViewModel + API Key + data from the new company + Created + Bad Request + Unauthorized + Internal Server Error + + + + Update a company + + + {"name":"string","cnpj":{"cnpjNumber":"string"},"address":{"street":"string","city":"string","zipCode":"string"},"telephone":"string","carSlots":0,"motorcycleSlots":0} + + company name + company UpdateViewModel + API key + company and its updated data + Success + Bad Request + Not Found + Internal Server Error + + + + Delete a company + + company name + API key + deleted company + Success + Unauthorized + Not Found + Internal Server Error + + + + Check API status + + API status + Ok + API Offline + + + + Validate API Key + + + + + + Get collection of vehicles + + collection of vehicles + Success + Unauthorized + Not Found + Internal Server Error + + + + Get vehicle by licensePlate + + vehicle data by licensePlate + Success + Unauthorized + Not Found + Internal Server Error + + + + Get collection of Ford vehicles. Only works with Admin privileges + + collection of Ford vehicles + Success + Unauthorized + Not Found + Internal Server Error + + + + Register a new vehicle + + + {"company":{"cnpj":{"cnpjNumber":"string"},"address":{"street":"string","city":"string","zipCode":"string"}},"licensePlate":"strings","brand":"string","model":"string","color":"string","type":1,"companyName":"string"} + + new vehicle data + Created + Bad Request + Unauthorized + Internal Server Error + + + + Update data of a registered vehicle + + updated data of vehicle + + {"company":{"cnpj":{"cnpjNumber":"string"},"address":{"street":"string","city":"string","zipCode":"string"}},"licensePlate":"strings","brand":"string","model":"string","color":"string","type":1,"companyName":"string"} + + Success + Bad Request + Unauthorized + Internal Server Error + + + + Delete vehicle by licensePlate + + data of deleted vehicle + Success + Unauthorized + Not Found + Internal Server Error + + + + Registers a vehicle departure + + vehicle license plate + API key + vehicle data + Success + Unauthorized + Not Found + Internal Server Error + + + + Reactivate a vehicle which already has been in the parking lot + + vehicle license plate + API key + reactivation confirmation + Success + Unauthorized + Not Found + Internal Server Error + + + diff --git a/ParkingLotManager.WebApi/Program.cs b/ParkingLotManager.WebApi/Program.cs new file mode 100644 index 0000000..7c2c60f --- /dev/null +++ b/ParkingLotManager.WebApi/Program.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using ParkingLotManager.WebApi; +using ParkingLotManager.WebApi.Data; +using ParkingLotManager.WebApi.DTOs.Mappings; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.Services; +using System.Text; + +var builder = WebApplication.CreateBuilder(args); + + +ConfigureAuthentication(builder); +ConfigureServices(builder); +ConfigureSwaggerApi(builder); +ConfigureDTOs(builder); +ConfigureMvc(builder); + +var app = builder.Build(); + +LoadConfiguration(app); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); + +static void ConfigureAuthentication(WebApplicationBuilder builder) +{ + var key = Encoding.ASCII.GetBytes(Configuration.JwtKey); + builder.Services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(x => + { + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false + }; + }); +} + +static void ConfigureSwaggerApi(WebApplicationBuilder builder) +{ + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(x => + { + x.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Parking Lot Manager API", + Description = @"A sample ASP.NET Core Web API to manage CRUD operations + on a Parking Lot Management context and other few things", + Contact = new OpenApiContact + { + Name = "Matheus Ribeiro", + Email = "mat.araujoribeiro@gmail.com", + Url = new Uri("https://www.linkedin.com/in/matheusarb/") + }, + License = new OpenApiLicense + { + Name = "Mit License" + }, + Version = "v1" + }); + + var xmlFile = "ParkingLotManagerWebApi.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + x.IncludeXmlComments(xmlPath); + }); +} + +static void ConfigureServices(WebApplicationBuilder builder) +{ + var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); + builder.Services.AddDbContext(options => options.UseSqlServer(connectionString)); + + builder.Services.AddTransient(); +} + +static void ConfigureMvc(WebApplicationBuilder builder) +{ + builder.Services.AddControllers().ConfigureApiBehaviorOptions(options => + { + options.SuppressModelStateInvalidFilter = true; + }); +} + +static void ConfigureDTOs(WebApplicationBuilder builder) +{ + builder.Services.AddAutoMapper(typeof(MappingDTOs)); +} + +static void LoadConfiguration(WebApplication app) +{ + //Configuration.ApiKey = app.Configuration.GetValue("ApiKey"); + + var apiKey = Configuration.ApiKey; + apiKey = app.Configuration.GetValue("ApiKey"); +} diff --git a/ParkingLotManager.WebApi/Properties/launchSettings.json b/ParkingLotManager.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..9bd264e --- /dev/null +++ b/ParkingLotManager.WebApi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:16990", + "sslPort": 44352 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5166", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7255;http://localhost:5166", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ParkingLotManager.WebApi/Services/Contracts/ITokenService.cs b/ParkingLotManager.WebApi/Services/Contracts/ITokenService.cs new file mode 100644 index 0000000..22c18a0 --- /dev/null +++ b/ParkingLotManager.WebApi/Services/Contracts/ITokenService.cs @@ -0,0 +1,8 @@ +using ParkingLotManager.WebApi.Models; + +namespace ParkingLotManager.WebApi.Services.Contracts; + +public interface ITokenService +{ + public string GenerateToken(User user); +} diff --git a/ParkingLotManager.WebApi/Services/TokenService.cs b/ParkingLotManager.WebApi/Services/TokenService.cs new file mode 100644 index 0000000..e20ac66 --- /dev/null +++ b/ParkingLotManager.WebApi/Services/TokenService.cs @@ -0,0 +1,31 @@ +using Microsoft.IdentityModel.Tokens; +using ParkingLotManager.WebApi.Models; +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using System.Security.Claims; +using ParkingLotManager.WebApi.Extensions; +using ParkingLotManager.WebApi.Services.Contracts; + +namespace ParkingLotManager.WebApi.Services; + +public class TokenService : ITokenService +{ + public virtual string GenerateToken(User user) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(Configuration.JwtKey); + var claims = user.GetClaims(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddHours(4), + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey(key), + SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + + return tokenHandler.WriteToken(token); + } +} diff --git a/ParkingLotManager.WebApi/ValueObjects/Address.cs b/ParkingLotManager.WebApi/ValueObjects/Address.cs new file mode 100644 index 0000000..7e0a06e --- /dev/null +++ b/ParkingLotManager.WebApi/ValueObjects/Address.cs @@ -0,0 +1,30 @@ +using Microsoft.IdentityModel.Tokens; +using ParkingLotManager.WebApi.Extensions; + +namespace ParkingLotManager.WebApi.ValueObjects; + +public class Address : ValueObject +{ + private Address() + { } + + public Address(string street, string city, string zipCode) + { + Street = street; + City = city; + ZipCode = zipCode; + } + + public string Street { get; private set; } + public string City { get; private set; } + public string ZipCode { get; private set; } + + public virtual Address Update(Address newAddress) + { + Street = newAddress.Street.IsNullOrEmptyOrWhiteSpace() ? Street : newAddress.Street; + City = newAddress.City.IsNullOrEmptyOrWhiteSpace() ? City : newAddress.City; + ZipCode = newAddress.City.IsNullOrEmptyOrWhiteSpace() ? ZipCode : newAddress.ZipCode; + + return this; + } +} diff --git a/ParkingLotManager.WebApi/ValueObjects/Cnpj.cs b/ParkingLotManager.WebApi/ValueObjects/Cnpj.cs new file mode 100644 index 0000000..1859808 --- /dev/null +++ b/ParkingLotManager.WebApi/ValueObjects/Cnpj.cs @@ -0,0 +1,24 @@ +using Microsoft.IdentityModel.Tokens; + +namespace ParkingLotManager.WebApi.ValueObjects; + +public class Cnpj : ValueObject +{ + private Cnpj() + { } + + public Cnpj(string cnpjNumber) + { + CnpjNumber = cnpjNumber.Replace(".", "").Replace("/", "").Replace("-", ""); + + if (!cnpjNumber.IsNullOrEmpty()) + { + if (!IsValid) + throw new Exception("Invalid CNPJ"); + } + } + + public string CnpjNumber { get; } + + public bool IsValid => CnpjNumber.Length == 14; +} diff --git a/ParkingLotManager.WebApi/ValueObjects/ValueObject.cs b/ParkingLotManager.WebApi/ValueObjects/ValueObject.cs new file mode 100644 index 0000000..28c9403 --- /dev/null +++ b/ParkingLotManager.WebApi/ValueObjects/ValueObject.cs @@ -0,0 +1,5 @@ +namespace ParkingLotManager.WebApi.ValueObjects; + +public class ValueObject +{ +} diff --git a/ParkingLotManager.WebApi/ViewModels/CompanyViewModels/RegisterCompanyViewModel.cs b/ParkingLotManager.WebApi/ViewModels/CompanyViewModels/RegisterCompanyViewModel.cs new file mode 100644 index 0000000..797b535 --- /dev/null +++ b/ParkingLotManager.WebApi/ViewModels/CompanyViewModels/RegisterCompanyViewModel.cs @@ -0,0 +1,47 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.ValueObjects; +using System.ComponentModel.DataAnnotations; + +namespace ParkingLotManager.WebApi.ViewModels.CompanyViewModels; + +public class RegisterCompanyViewModel : Company +{ + public RegisterCompanyViewModel( + string name, + Cnpj cnpj, + Address address, + string telephone, + int carSlots, + int motorcycleSlots) + { + Name = name; + Cnpj = cnpj; + Address = address; + Telephone = telephone; + CarSlots = carSlots; + MotorcycleSlots = motorcycleSlots; + } + + [Required(ErrorMessage = "Company name is required")] + [MinLength(3, ErrorMessage ="Company name must have at least 3 characters")] + public string Name { get; set; } + + [Required(ErrorMessage = "Company CNPJ is required")] + public Cnpj Cnpj { get; set; } + + [Required(ErrorMessage = "Company address is required")] + public Address Address { get; set; } + + [Required(ErrorMessage = "Company telephone is required")] + public string Telephone { get; set; } + + [Required(ErrorMessage = "Company car slots is required")] + [Range(minimum:1, maximum:int.MaxValue)] + public int CarSlots { get; set; } + + [Required(ErrorMessage = "Company motorcycle slots is required")] + [Range(minimum:1, maximum:int.MaxValue)] + public int MotorcycleSlots { get; set; } +} + diff --git a/ParkingLotManager.WebApi/ViewModels/CompanyViewModels/UpdateCompanyViewModel.cs b/ParkingLotManager.WebApi/ViewModels/CompanyViewModels/UpdateCompanyViewModel.cs new file mode 100644 index 0000000..45ca167 --- /dev/null +++ b/ParkingLotManager.WebApi/ViewModels/CompanyViewModels/UpdateCompanyViewModel.cs @@ -0,0 +1,45 @@ +using ParkingLotManager.WebApi.ValueObjects; +using ParkingLotManager.WebApi.ViewModels.VehicleViewModels; +using System.ComponentModel.DataAnnotations; + +namespace ParkingLotManager.WebApi.ViewModels.CompanyViewModels; + +public class UpdateCompanyViewModel +{ + public UpdateCompanyViewModel() + { + } + + public UpdateCompanyViewModel(string? name, Cnpj? cnpj, Address? address, string? telephone, int carSlots, int motorcycleSlots) + { + Name = name; + Cnpj = cnpj; + Address = address; + Telephone = telephone; + CarSlots = carSlots == 0 ? CarSlots : carSlots; + MotorcycleSlots = motorcycleSlots == 0 ? MotorcycleSlots : motorcycleSlots; + } + + public string? Name { get; set; } + public Cnpj? Cnpj { get; set; } + public Address? Address { get; set; } + public string? Telephone { get; set; } + public int CarSlots { get; set; } + public int MotorcycleSlots { get; set; } + + public bool CheckIfAllEmpty(UpdateVehicleViewModel viewModel) + { + Type type = viewModel.GetType(); + var props = type.GetProperties(); + var count = 0; + + foreach (var prop in props) + { + var value = prop.GetValue(viewModel); + if (value == "" || prop.PropertyType.IsValueType && value.Equals(Activator.CreateInstance(prop.PropertyType))) + count++; + } + + return count == props.Length; + } +} diff --git a/ParkingLotManager.WebApi/ViewModels/ResultViewModel.cs b/ParkingLotManager.WebApi/ViewModels/ResultViewModel.cs new file mode 100644 index 0000000..a0446bd --- /dev/null +++ b/ParkingLotManager.WebApi/ViewModels/ResultViewModel.cs @@ -0,0 +1,30 @@ +using System.Net; + +namespace ParkingLotManager.WebApi.ViewModels; + +public class ResultViewModel +{ + public ResultViewModel(T data, List errors) + { + Data = data; + Errors = errors; + } + + public ResultViewModel(T data) + { + Data = data; + } + + public ResultViewModel(List errors) + { + Errors = errors; + } + + public ResultViewModel(string error) + { + Errors.Add(error); + } + + public T? Data { get; private set; } + public List Errors { get; private set; } = new(); +} diff --git a/ParkingLotManager.WebApi/ViewModels/UserViewModels/CreateUserViewModel.cs b/ParkingLotManager.WebApi/ViewModels/UserViewModels/CreateUserViewModel.cs new file mode 100644 index 0000000..2328afc --- /dev/null +++ b/ParkingLotManager.WebApi/ViewModels/UserViewModels/CreateUserViewModel.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace ParkingLotManager.WebApi.ViewModels.UserViewModels; + +public class CreateUserViewModel +{ + public CreateUserViewModel(string name, string email, string companyName) + { + Name = name; + Email = email; + CompanyName = companyName; + } + + [Required(ErrorMessage = "Name is required")] + public string Name { get; set; } = string.Empty; + + [EmailAddress] + [Required(ErrorMessage = "Email is required")] + public string Email { get; set; } = string.Empty; + + [Required(ErrorMessage = "Company name is required")] + public string CompanyName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/ParkingLotManager.WebApi/ViewModels/UserViewModels/LoginViewModel.cs b/ParkingLotManager.WebApi/ViewModels/UserViewModels/LoginViewModel.cs new file mode 100644 index 0000000..075b448 --- /dev/null +++ b/ParkingLotManager.WebApi/ViewModels/UserViewModels/LoginViewModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace ParkingLotManager.WebApi.ViewModels.UserViewModels; + +public class LoginViewModel +{ + [Required(ErrorMessage ="Type your email")] + [EmailAddress(ErrorMessage ="Email is invalid")] + public string Email { get; set; } + + [Required(ErrorMessage ="Type your password")] + [MinLength(6, ErrorMessage ="Password must contain at least 6 characters")] + public string Password { get; set; } +} diff --git a/ParkingLotManager.WebApi/ViewModels/UserViewModels/UpdateUserViewModel.cs b/ParkingLotManager.WebApi/ViewModels/UserViewModels/UpdateUserViewModel.cs new file mode 100644 index 0000000..4d16023 --- /dev/null +++ b/ParkingLotManager.WebApi/ViewModels/UserViewModels/UpdateUserViewModel.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace ParkingLotManager.WebApi.ViewModels.UserViewModels; + +public class UpdateUserViewModel +{ + public UpdateUserViewModel() + { + } + + public UpdateUserViewModel(string? email, string? name, string? passwordHash) + { + Email = email; + Name = name; + PasswordHash = passwordHash; + } + + [JsonPropertyName("Email")] + [EmailAddress] + public string? Email { get; set; } + [JsonPropertyName("Name")] + public string? Name { get; set; } + [JsonPropertyName("PasswordHash")] + public string? PasswordHash { get; set; } +} diff --git a/ParkingLotManager.WebApi/ViewModels/VehicleViewModels/RegisterVehicleViewModel.cs b/ParkingLotManager.WebApi/ViewModels/VehicleViewModels/RegisterVehicleViewModel.cs new file mode 100644 index 0000000..ff051f9 --- /dev/null +++ b/ParkingLotManager.WebApi/ViewModels/VehicleViewModels/RegisterVehicleViewModel.cs @@ -0,0 +1,44 @@ +using ParkingLotManager.WebApi.Enums; +using ParkingLotManager.WebApi.Models; +using System.ComponentModel.DataAnnotations; + +namespace ParkingLotManager.WebApi.ViewModels.VehicleViewModels; + +public class RegisterVehicleViewModel : Vehicle +{ + public RegisterVehicleViewModel( + string licensePlate, + string brand, + string model, + string color, + EVehicleType type, + string companyName) + { + LicensePlate = licensePlate.Replace("-", "").ToUpper(); + Brand = brand; + Model = model; + Color = color; + Type = type; + CompanyName = companyName; + } + + [Required(ErrorMessage = "License plate is required")] + [MinLength(7, ErrorMessage = "License plate must have at least 7 characters")] + [MaxLength(8, ErrorMessage = "License plate must not have more then 8 characters")] + public string LicensePlate { get; private set; } + + [Required(ErrorMessage = "Brand is required")] + public string Brand { get; private set; } + + [Required(ErrorMessage = "Model is required")] + public string Model { get; private set; } + + [Required(ErrorMessage = "Color is required")] + public string Color { get; private set; } + + [Required(ErrorMessage = "Type is required")] + public EVehicleType Type { get; private set; } + + [Required(ErrorMessage = "Company name is required")] + public string CompanyName { get; private set; } +} diff --git a/ParkingLotManager.WebApi/ViewModels/VehicleViewModels/UpdateVehicleViewModel.cs b/ParkingLotManager.WebApi/ViewModels/VehicleViewModels/UpdateVehicleViewModel.cs new file mode 100644 index 0000000..87e6b11 --- /dev/null +++ b/ParkingLotManager.WebApi/ViewModels/VehicleViewModels/UpdateVehicleViewModel.cs @@ -0,0 +1,45 @@ +using ParkingLotManager.WebApi.Enums; +using ParkingLotManager.WebApi.Models; +using System.ComponentModel; + +namespace ParkingLotManager.WebApi.ViewModels.VehicleViewModels; + +public class UpdateVehicleViewModel +{ + public UpdateVehicleViewModel() + { + } + + public UpdateVehicleViewModel(string? licensePlate, string? brand, string? model, string? color, EVehicleType type, string? companyName) + { + LicensePlate = licensePlate; + Brand = brand; + Model = model; + Color = color; + Type = type; + CompanyName = companyName; + } + + public string? LicensePlate { get; set; } + public string? Brand { get; set; } + public string? Model { get; set; } + public string? Color { get; set; } + public EVehicleType Type { get; set; } + public string? CompanyName { get; set; } + + public bool CheckIfAllEmpty(UpdateVehicleViewModel viewModel) + { + Type type = viewModel.GetType(); + var props = type.GetProperties(); + var count = 0; + + foreach (var prop in props) + { + var value = prop.GetValue(viewModel); + if (value == "" || prop.PropertyType.IsValueType && value.Equals(Activator.CreateInstance(prop.PropertyType))) + count++; + } + + return count == props.Length; + } +} diff --git a/ParkingLotManager.WebApi/appsettings.Development.json b/ParkingLotManager.WebApi/appsettings.Development.json new file mode 100644 index 0000000..af2f4cd --- /dev/null +++ b/ParkingLotManager.WebApi/appsettings.Development.json @@ -0,0 +1,14 @@ +{ + "JwtKey": "IxN9fUjnX0OcZfUl3W44ew==!!!!====", + "ApiKeyName": "api_key", + "ApiKey": "parking_oPt4oylWx0X4wfnj", + "ConnectionStrings": { + "DefaultConnection": "Server=[insert_serverName_port];Database=ParkingLotManager;User ID=[insert_UserID];Password=[insert_server_Password];TrustServerCertificate=True" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ParkingLotManager.WebApi/appsettings.json b/ParkingLotManager.WebApi/appsettings.json new file mode 100644 index 0000000..ec04bc1 --- /dev/null +++ b/ParkingLotManager.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/ParkingLotManager.XUnitTests/Controllers/AccountControllerTests.cs b/ParkingLotManager.XUnitTests/Controllers/AccountControllerTests.cs new file mode 100644 index 0000000..281bc2d --- /dev/null +++ b/ParkingLotManager.XUnitTests/Controllers/AccountControllerTests.cs @@ -0,0 +1,92 @@ +using Microsoft.AspNetCore.Mvc; +using Moq; +using ParkingLotManager.WebApi; +using ParkingLotManager.WebApi.Controllers; +using ParkingLotManager.WebApi.Data; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.ViewModels.CompanyViewModels; +using ParkingLotManager.WebApi.ViewModels.UserViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ParkingLotManager.XUnitTests.Controllers; + +[Trait("AccountController", "Unit")] +public class AccountControllerTests +{ + private readonly string _apiKeyName = Configuration.ApiKeyName; + private Mock _mockedDbContext = new(); + private CreateUserViewModel _createUserViewModel = new( + "test", "test", "compTest"); + private UpdateUserViewModel _updateUserViewModel = new( + "test", "test", "pass12345"); + + [Fact(DisplayName ="Return User by Id")] + public async Task Get_ShouldReturnOkWhenGettingUserById() + { + var mockedController = new Mock(); + + mockedController.Setup(m => m.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new OkObjectResult(new User())); + + var expected = await mockedController.Object.GetByIdAsync(1, _apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Get_ShouldReturnOkWhenGettingAllUsers() + { + var mockedController = new Mock(); + + mockedController.Setup(m => m.GetAsync(It.IsAny())) + .ReturnsAsync(new OkObjectResult(new List())); + + var expected = await mockedController.Object.GetAsync(_apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Create_ShouldReturnOkWhenCreatingAUser() + { + var mockedController = new Mock(); + + mockedController.Setup(m => m.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new OkObjectResult(new User())); + + var expected = await mockedController.Object.CreateAsync(_createUserViewModel, _apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Update_ShouldReturnOkWhenUpdatingAUser() + { + var mockedController = new Mock(); + + mockedController.Setup(m => m.Update(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new OkObjectResult(new User())); + + var expected = await mockedController.Object.Update(1, _updateUserViewModel, _apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Delete_ShouldReturnOkWhenDeletingACompany() + { + var mockedController = new Mock(); + + mockedController.Setup(m => m.Delete(It.IsAny(), It.IsAny())) + .ReturnsAsync(new OkObjectResult(new Company())); + + var expected = await mockedController.Object.Delete(1, _apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } +} diff --git a/ParkingLotManager.XUnitTests/Controllers/CompanyControllerTests.cs b/ParkingLotManager.XUnitTests/Controllers/CompanyControllerTests.cs new file mode 100644 index 0000000..fbac625 --- /dev/null +++ b/ParkingLotManager.XUnitTests/Controllers/CompanyControllerTests.cs @@ -0,0 +1,94 @@ +using Microsoft.AspNetCore.Mvc; +using Moq; +using ParkingLotManager.WebApi; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.Controllers; +using ParkingLotManager.WebApi.Data; +using ParkingLotManager.WebApi.ValueObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using ParkingLotManager.WebApi.ViewModels.CompanyViewModels; + +namespace ParkingLotManager.XUnitTests.Controllers; + +[Trait("CompanyController", "Unit")] +public class CompanyControllerTests +{ + private Mock _mockedDbContext = new(); + private string _apiKeyName = Configuration.ApiKeyName; + private RegisterCompanyViewModel _registerCompanyViewModel = new( + "Comp", new Cnpj("11111111111111"), new Address("street", "city", "0000000"), "8199999999", 15, 15); + private UpdateCompanyViewModel _updateCompanyViewModel = new UpdateCompanyViewModel( + "Comp", new Cnpj("11111111111111"), new Address("street", "city", "0000000"), "8199999999", 50, 50); + + [Fact] + public async Task Get_ShouldReturnOkWhenGettingCompanyByName() + { + var mockedController = new Mock(); + + mockedController.Setup(m => m.GetAsyncByName(It.IsAny(), It.IsAny())) + .ReturnsAsync(new OkObjectResult(new Company())); + + var expected = await mockedController.Object.GetAsyncByName("Comp", _apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Get_ShouldReturnOkWhenGettingAllCompanies() + { + var mockedController = new Mock(); + + mockedController.Setup(m => m.GetAsync(It.IsAny())) + .ReturnsAsync(new OkObjectResult(new List())); + + var expected = await mockedController.Object.GetAsync(_apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Create_ShouldReturnOkWhenCreatingACompany() + { + var mockedController = new Mock(); + + mockedController.Setup(m => m.RegisterAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new OkObjectResult(new Company())); + + var expected = await mockedController.Object.RegisterAsync(_registerCompanyViewModel, _apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Update_ShouldReturnOkWhenUpdatingACompany() + { + var mockedController = new Mock(); + + mockedController.Setup(m => m.Update(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new OkObjectResult(new Company())); + + var expected = await mockedController.Object.Update("Comp", _updateCompanyViewModel, _apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Delete_ShouldReturnOkWhenDeletingACompany() + { + var mockedController = new Mock(); + + mockedController.Setup(m => m.Delete(It.IsAny(), It.IsAny())) + .ReturnsAsync(new OkObjectResult(new Company())); + + var expected = await mockedController.Object.Delete("Comp", _apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + +} diff --git a/ParkingLotManager.XUnitTests/Controllers/VehicleControllerTests.cs b/ParkingLotManager.XUnitTests/Controllers/VehicleControllerTests.cs new file mode 100644 index 0000000..de74530 --- /dev/null +++ b/ParkingLotManager.XUnitTests/Controllers/VehicleControllerTests.cs @@ -0,0 +1,99 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Moq; +using ParkingLotManager.WebApi; +using ParkingLotManager.WebApi.Controllers; +using ParkingLotManager.WebApi.Data; +using ParkingLotManager.WebApi.Enums; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.ViewModels.VehicleViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ParkingLotManager.XUnitTests.Controllers; + +[Trait("VehicleController", "Unit")] +public class VehicleControllerTests +{ + private Mock _mockedContext = new(); + private readonly string apiKeyName = Configuration.ApiKeyName; + private readonly RegisterVehicleViewModel _registerVehicleViewModel = new RegisterVehicleViewModel( + "AAA0000", "GM", "F-50", "Black", EVehicleType.Car, "Park4you"); + private readonly UpdateVehicleViewModel _updateVehicleViewModel = new( + "AAA0000", "GM", "F-1000", "Red", EVehicleType.Car, "Park4you"); + + [InlineData("AAA0000")] + [Theory] + public async Task Get_ShouldGetVehicleByLicensePlateAsync(string licensePlate) + { + var dbVehicle = new Vehicle(); + var _mockedController = new Mock(_mockedContext.Object); + + _mockedController.Setup(m => m.GetByLicensePlateAsync(It.IsAny(), Configuration.ApiKeyName)) + .ReturnsAsync(new OkObjectResult(dbVehicle)); + + var expected = await _mockedController.Object.GetByLicensePlateAsync(licensePlate, apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task GetAll_ShouldGetAllVehiclesAsync() + { + var _mockedController = new Mock(_mockedContext.Object); + + _mockedController.Setup(m => m.GetAsync(Configuration.ApiKeyName)) + .ReturnsAsync(new OkObjectResult(new List())); + + var expected = await _mockedController.Object.GetAsync(apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Create_ShouldRegisterAVehicle() + { + var createdVehicle = new Vehicle(); + var _mockedController = new Mock(_mockedContext.Object); + + _mockedController.Setup(m => m.RegisterAsync(It.IsAny(), apiKeyName)) + .ReturnsAsync(new OkObjectResult(createdVehicle)); + + var expected = await _mockedController.Object.RegisterAsync(_registerVehicleViewModel, apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Update_ShouldUpdateAVehicle() + { + var createdVehicle = new Vehicle(); + var updatedVehicle = createdVehicle.Update(_updateVehicleViewModel); + var _mockedController = new Mock(_mockedContext.Object); + + _mockedController.Setup(m => m.Update(It.IsAny(), It.IsAny(), apiKeyName)) + .ReturnsAsync(new OkObjectResult(updatedVehicle)); + + var expected = await _mockedController.Object.Update("", _updateVehicleViewModel, apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } + + [Fact] + public async Task Delete_ShouldDeleteAVehicle() + { + var createdVehicle = new Vehicle(); + var _mockedController = new Mock(_mockedContext.Object); + + _mockedController.Setup(m => m.Delete(It.IsAny(), apiKeyName)) + .ReturnsAsync(new OkObjectResult("")); + + var expected = await _mockedController.Object.Delete("AAA0000", apiKeyName); + + Assert.Equal(200, ((OkObjectResult)expected).StatusCode); + } +} \ No newline at end of file diff --git a/ParkingLotManager.XUnitTests/Entities/CompanyTests.cs b/ParkingLotManager.XUnitTests/Entities/CompanyTests.cs new file mode 100644 index 0000000..ee90288 --- /dev/null +++ b/ParkingLotManager.XUnitTests/Entities/CompanyTests.cs @@ -0,0 +1,48 @@ +using Moq; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.ViewModels.CompanyViewModels; +using ParkingLotManager.WebApi.ValueObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ParkingLotManager.XUnitTests.Entities; + +[Trait("CompanyEntity", "Unit")] +public class CompanyTests +{ + private Company _company = new(); + private Mock _mockedCompany = new(); + private readonly Address _address = new("street", "city", "000000"); + private readonly RegisterCompanyViewModel _registerCompanyViewModel = new RegisterCompanyViewModel( + "name", new Cnpj("00000000000000"), new Address("street", "city", "000000"), "659999999", 10, 10); + private readonly UpdateCompanyViewModel _updateCompanyViewModel = new UpdateCompanyViewModel( + "comp", new Cnpj("11111111111111"), new Address("street2", "city2", "1111111"), "3388888888", 15, 15); + + [Fact] + public void Create_ShouldCreateACompany() + { + _mockedCompany.Setup(m => m.Create(It.IsAny())) + .Returns(_company); + + var actual = _company.Create(_registerCompanyViewModel); + var expected = _mockedCompany.Object.Create(_registerCompanyViewModel); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Update_ShouldUpdateACompany() + { + _mockedCompany.Setup(m => m.Update(It.IsAny(), It.IsAny
())) + .Returns(_company); + + var actual = _company.Update(_updateCompanyViewModel, _address); + var expected = _mockedCompany.Object.Update(_updateCompanyViewModel, _address); + + Assert.Equal(expected, actual); + } +} diff --git a/ParkingLotManager.XUnitTests/Entities/UserTests.cs b/ParkingLotManager.XUnitTests/Entities/UserTests.cs new file mode 100644 index 0000000..337dbc9 --- /dev/null +++ b/ParkingLotManager.XUnitTests/Entities/UserTests.cs @@ -0,0 +1,52 @@ +using Moq; +using ParkingLotManager.WebApi.Controllers; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.ViewModels.UserViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ParkingLotManager.XUnitTests.Entities; + +[Trait("Entities", "Unit")] +public class UserTests +{ + private User _user = new(); + private Mock _mockedUser = new(); + private readonly CreateUserViewModel _createUserViewModel = new CreateUserViewModel("superman", "superman@io.com", "parkingManagement"); + private readonly UpdateUserViewModel _updateUserViewModel = new UpdateUserViewModel("batman@io.com", "Batman", "abc123"); + private readonly string _password = "123456"; + + [Fact] + public void Create_ShouldCreateAUser() + { + // 1. Arrange phase + _mockedUser.Setup(m => m.Create(It.IsAny(), _password)) + .Returns(_user); + + // 2. Act phase + var actual = _user.Create(_createUserViewModel, _password); + var expected = _mockedUser.Object.Create(_createUserViewModel, _password); + + // 3. Assert phase + Assert.Equal(expected, actual); + } + + [Fact] + public void Update_ShouldUpdateAUser() + { + //1 + _mockedUser.Setup(m => m.Update(It.IsAny())) + .Returns(_user); + + //2 + var actual = _user.Update(_updateUserViewModel); + var expected = _mockedUser.Object.Update(_updateUserViewModel); + + //3 + Assert.Equal(expected, actual); + } +} diff --git a/ParkingLotManager.XUnitTests/Entities/VehicleTests.cs b/ParkingLotManager.XUnitTests/Entities/VehicleTests.cs new file mode 100644 index 0000000..46bc1d6 --- /dev/null +++ b/ParkingLotManager.XUnitTests/Entities/VehicleTests.cs @@ -0,0 +1,61 @@ +using Moq; +using ParkingLotManager.WebApi; +using ParkingLotManager.WebApi.Enums; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.Models.Contracts; +using ParkingLotManager.WebApi.ViewModels.VehicleViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.ViewModels.VehicleViewModels; +using ParkingLotManager.WebApi.Controllers; +using ParkingLotManager.WebApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace ParkingLotManager.XUnitTests.Entities; + +public sealed class VehicleTests +{ + private Vehicle _vehicle = new(); + private Mock _mockedVehicle = new Mock(); + private Mock _mockedDbContext = new(); + public readonly RegisterVehicleViewModel _registerVehicleViewModel = new RegisterVehicleViewModel("AAA0000", "Ferrari", "f-50", "red", EVehicleType.Car, "WellPark Inc"); + private readonly UpdateVehicleViewModel _updateVehicleViewModel = new UpdateVehicleViewModel("AAA0000", "Ferrari", "f-50", "red", EVehicleType.Motorcycle, "WellPark Inc"); + + [Fact] + public void Create_ShouldCreateAVehicle() + { + //1. Arrange + _mockedVehicle.Setup(x => x.Create(It.IsAny())) + .Returns(_vehicle); + + //2. Act + var actual = _vehicle.Create(_registerVehicleViewModel); + var expected = _mockedVehicle.Object.Create(_registerVehicleViewModel); + + //3. Assert + Assert.Equal(expected, actual); + } + + [Fact] + public void Update_ShouldUpdateAVehicle() + { + //1 + var dbVehicle = new Vehicle(); + _mockedVehicle.Setup(m => m.Update(It.IsAny())).Returns(dbVehicle); + + //2 + var actual = dbVehicle.Update(_updateVehicleViewModel); + var expected = _mockedVehicle.Object.Update(_updateVehicleViewModel); + + //3 + Assert.Equal(expected, actual); + } +} diff --git a/ParkingLotManager.XUnitTests/ParkingLotManager.XUnitTests.csproj b/ParkingLotManager.XUnitTests/ParkingLotManager.XUnitTests.csproj new file mode 100644 index 0000000..fe8339a --- /dev/null +++ b/ParkingLotManager.XUnitTests/ParkingLotManager.XUnitTests.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/ParkingLotManager.XUnitTests/Services/FlowManagementServiceTests.cs b/ParkingLotManager.XUnitTests/Services/FlowManagementServiceTests.cs new file mode 100644 index 0000000..e5703ec --- /dev/null +++ b/ParkingLotManager.XUnitTests/Services/FlowManagementServiceTests.cs @@ -0,0 +1,25 @@ +using Moq; +using ParkingLotManager.ReportApi.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ParkingLotManager.XUnitTests.Services; + +public class FlowManagementServiceTests +{ + private readonly Mock _mockedService = new(); + + [Fact] + public void Should_return_amount_of_entered_vehicles() + { + _mockedService.Setup(x => x.CheckInFlowCalc()) + .ReturnsAsync(1); + + var result = _mockedService.Object.CheckInFlowCalc(); + Assert.IsType>(result); + } +} diff --git a/ParkingLotManager.XUnitTests/Services/TokenServiceTests.cs b/ParkingLotManager.XUnitTests/Services/TokenServiceTests.cs new file mode 100644 index 0000000..b793ab4 --- /dev/null +++ b/ParkingLotManager.XUnitTests/Services/TokenServiceTests.cs @@ -0,0 +1,27 @@ +using Moq; +using ParkingLotManager.WebApi.Models; +using ParkingLotManager.WebApi.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ParkingLotManager.XUnitTests.Services; + +public class TokenServiceTests +{ + [Fact] + public void Should_return_token_if_User_is_given() + { + var mockedService = new Mock(); + var user = new User(); + + mockedService.Setup(x => x.GenerateToken(It.IsAny())) + .Returns("fixed_string"); + + var mockedToken = mockedService.Object.GenerateToken(user); + Assert.Equal("fixed_string", mockedToken); + } +} diff --git a/dotnet-test.sln b/dotnet-test.sln new file mode 100644 index 0000000..a70b87b --- /dev/null +++ b/dotnet-test.sln @@ -0,0 +1,62 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParkingLotManager.WebApi", "ParkingLotManager.WebApi\ParkingLotManager.WebApi.csproj", "{10CB257A-6F26-45A1-9F10-020F1249C21A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParkingLotManager.ReportApi", "ParkingLotManager.ReportApi\ParkingLotManager.ReportApi.csproj", "{B70A4180-6FD7-4317-8ECD-0FF68495F4CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParkingLotManager.XUnitTests", "ParkingLotManager.XUnitTests\ParkingLotManager.XUnitTests.csproj", "{101FF11A-EC52-4971-8E95-3F5DC729CB63}" +EndProject +Global + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Debug|x64.ActiveCfg = Debug|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Debug|x64.Build.0 = Debug|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Debug|x86.ActiveCfg = Debug|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Debug|x86.Build.0 = Debug|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Release|Any CPU.Build.0 = Release|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Release|x64.ActiveCfg = Release|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Release|x64.Build.0 = Release|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Release|x86.ActiveCfg = Release|Any CPU + {10CB257A-6F26-45A1-9F10-020F1249C21A}.Release|x86.Build.0 = Release|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Debug|x64.Build.0 = Debug|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Debug|x86.ActiveCfg = Debug|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Debug|x86.Build.0 = Debug|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Release|Any CPU.Build.0 = Release|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Release|x64.ActiveCfg = Release|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Release|x64.Build.0 = Release|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Release|x86.ActiveCfg = Release|Any CPU + {B70A4180-6FD7-4317-8ECD-0FF68495F4CA}.Release|x86.Build.0 = Release|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Debug|x64.ActiveCfg = Debug|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Debug|x64.Build.0 = Debug|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Debug|x86.ActiveCfg = Debug|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Debug|x86.Build.0 = Debug|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Release|Any CPU.Build.0 = Release|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Release|x64.ActiveCfg = Release|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Release|x64.Build.0 = Release|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Release|x86.ActiveCfg = Release|Any CPU + {101FF11A-EC52-4971-8E95-3F5DC729CB63}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal