Compare commits

..

79 Commits

Author SHA1 Message Date
801475654e 0.0.1
Reviewed-on: #9
2024-12-25 23:49:45 +04:00
2dcd5df336 Merge pull request 'front' (#8) from front into dev
Reviewed-on: #8
2024-12-25 23:49:21 +04:00
2febb000ba Отчеты 2024-12-25 23:33:52 +04:00
8715e5949b add: ссылка на статистику 2024-12-24 03:43:41 +04:00
433f6f5926 fix: футер теперь внизу 2024-12-24 03:26:06 +04:00
c1f785eff1 add: добавлена страница теплицы 2024-12-24 03:25:52 +04:00
1c5313a1d2 Merge branch 'front' into front-greenhouse 2024-12-23 20:50:29 +04:00
294bf384b4 add: поддержка swagget-typescript-api. теперь можно генерировать методы API на фронте 2024-12-23 20:48:44 +04:00
c8265a5209 Merge branch 'dev' into front 2024-12-23 19:39:18 +04:00
3393c1be70 демо страницы 2024-12-23 19:36:40 +04:00
2b606be0cc Merge branch 'front' into front-greenhouse 2024-12-23 18:55:22 +04:00
7ed5bc7bc6 Merge pull request 'greenhouseCRUD' (#7) from greenhouseCRUD into dev
Reviewed-on: #7
2024-12-15 20:52:57 +04:00
1c6f0830e3 Merge branch 'devManager' into greenhouseCRUD 2024-12-04 23:25:35 +04:00
aace337052 fix: хз, вроде бы работает, но почему-то не подключается 2024-12-04 23:20:09 +04:00
the
71bc31d0bd containers test 2024-12-04 21:33:02 +04:00
db3cbde0af add: основной круд 2024-12-04 11:41:08 +04:00
7b751b7072 fix: ничего не помогает нормально запросить данные... это конец... 2024-12-04 11:32:34 +04:00
739d434f53 fix: есть контакт! между облаком и кафкой 2024-12-04 06:27:39 +04:00
2e20e9d7cc fix: вынесены важные параметры в переменные окружения 2024-12-04 04:06:56 +04:00
1ddc8d9556 Merge branch 'dev' into greenhouseCRUD 2024-12-04 02:13:27 +04:00
8478bf1a47 add: greenhouse service 2024-12-04 01:57:29 +04:00
9c1720e131 fix: kafka service 2024-12-04 01:57:10 +04:00
m.zargarov
9c0aad605c add Redis service 2024-12-01 19:33:52 +04:00
Аришина)
37abfaf7b2 страница теплиц добавлена 2024-11-20 11:56:13 +04:00
Аришина)
a75a137e35 верстку поменял 2024-11-20 10:32:43 +04:00
fed96d5b86 del: сервисы брокера
они уже есь
2024-11-19 23:42:12 +04:00
9b770d131a Merge branch 'dev' into greenhouseCRUD 2024-11-19 23:35:40 +04:00
c34926e1ec Merge branch 'dev' of https://git.is.ulstu.ru/mfnefd/Cucumber into dev 2024-11-19 23:33:28 +04:00
d03313041c Merge branch 'devManager' into dev 2024-11-19 23:33:16 +04:00
c230d86404 add: сервис брокера, сущности брокера 2024-11-19 23:23:21 +04:00
07c5ea7853 Merge pull request 'brokerService' (#6) from brokerService into dev
Reviewed-on: #6
2024-11-19 19:46:42 +04:00
5e73961ad5 Add Valve Controller 2024-11-19 19:36:05 +04:00
53d93635fc Merge branch 'dev' into greenhouseCRUD 2024-11-19 16:45:19 +04:00
897b6be34a Merge remote-tracking branch 'origin/devManager' into dev 2024-11-19 16:43:03 +04:00
e8a1a8385b Merge branch 'dev' into brokerService 2024-11-13 15:42:26 +04:00
7c310d21f7 Add broker service 2024-11-13 15:24:47 +04:00
5687949f96 todo: в докерфайл добавить инициализацию бд 2024-11-13 03:33:49 +04:00
2a9508f737 add: контроллер теплицы, добавлен сервис брокера в DI 2024-11-13 03:33:03 +04:00
1e2bd05667 add: интерфейсы брокера, сущности топиков брокера 2024-11-13 03:32:30 +04:00
fbfde769b1 add: сущность теплицы 2024-11-13 03:31:43 +04:00
49c7d14925 Merge branch 'FarmCRUD' into dev
mac to ip, dockerfile and docker-compose
2024-11-13 02:08:51 +04:00
3ce4d6baf2 fix: dockerfile и docker-compose 2024-11-13 01:49:13 +04:00
the
5fa9c76b99 test changes 2024-11-13 01:14:32 +04:00
the
c032253699 фикс2 2024-11-13 01:00:16 +04:00
the
83aec339c9 фикс 2024-11-13 00:53:31 +04:00
the
b009ebdd0c АААААААААААА 2024-11-13 00:36:31 +04:00
the
73961934f0 СТОЛЬКО МУЧЕНИЙ РАДИ ОДНОЙ СТРОЧКИ, ЕЩЁ И НЕТВОРК НА САМОМ ДЕЛЕ НЕ РАБОТАЕТ 2024-11-13 00:31:21 +04:00
the
ffef39d409 СТОЛЬКО МУЧЕНИЙ РАДИ ОДНОЙ СТРОЧКИ 2024-11-13 00:30:33 +04:00
the
57e05aba90 kafka network unfix 2024-11-12 22:34:06 +04:00
the
ce3f3a4dc6 kafka network fix 2024-11-12 22:31:57 +04:00
5adec563ac add: dockerfile и изменен чутка docker-compose 2024-11-12 19:57:44 +04:00
488c91d2b1 del: удалены ненужные файлы жесть 2024-11-12 19:48:58 +04:00
03302065ab fix: mac теперь не ипользуется. ip наше все 2024-11-12 19:18:37 +04:00
the
4747f975c5 kafka network 2024-11-12 18:03:45 +04:00
the
f82c8daa92 detector webhooks 2024-11-12 17:04:24 +04:00
the
b7f4aa3f9f webhook fix 2024-11-12 16:19:37 +04:00
the
7f5262575e webhook test 2024-11-12 16:18:14 +04:00
80b002f12a Merge pull request 'add redis' (#3) from add-redis into dev
Reviewed-on: #3
2024-11-10 13:36:59 +04:00
m.zargarov
a4dae6211f add redis 2024-11-10 13:35:23 +04:00
Аришина)
5d7beec9c1 Добавил cors program.cs для фронта 2024-10-30 15:33:08 +04:00
Аришина)
0abb511375 Регистрация и вход работают 2024-10-30 15:31:57 +04:00
3f5bb31646 Менеджер 2024-10-30 11:10:39 +04:00
5be5bb06bb Перенос 2024-10-30 11:06:40 +04:00
1c38c61fbc о 2024-10-29 19:50:35 +04:00
091dcbd3a3 Управление вентилями/нагревателями 2024-10-29 19:49:33 +04:00
the
7f88f87722 fix, no more infinite loops for consumer 2024-10-29 17:51:20 +04:00
the
6291bb483c I MUST JOB 2024-10-29 17:31:47 +04:00
the
08ee12aa8b simple detector + manager 2024-10-29 16:07:02 +04:00
78aea1397e Merge pull request 'FarmCRUD' (#2) from FarmCRUD into dev
Reviewed-on: #2
2024-10-29 15:23:11 +04:00
Аришина)
d8aa0bea59 Спидран по фронтенду погнали 2024-10-29 09:21:00 +04:00
c4899d4a11 Make Farm CRUD 2024-10-29 00:40:58 +04:00
33c3a074da Make Farm CRUD 2024-10-29 00:38:46 +04:00
the
720cf4bd60 connection test 2024-10-28 19:43:13 +04:00
m.zargarov
0bf5823652 add jwt auth 2024-10-28 18:39:13 +04:00
the
d16968bc98 compose + kafka ui 2024-10-28 14:47:20 +04:00
the
3fa35ba617 init 2024-10-28 13:14:46 +04:00
m.zargarov
0c94c926a5 Add users table 2024-10-28 02:09:55 +04:00
m.zargarov
c58d07731c add dev 2024-10-28 00:25:51 +04:00
m.zargarov
46df076f96 add .gitignore 2024-10-28 00:02:57 +04:00
119 changed files with 36550 additions and 130 deletions

25
.dockerignore Normal file
View File

@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

4
.env Normal file
View File

@ -0,0 +1,4 @@
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="12345"
POSTGRES_DB="main_database"
DB_CONNECTION_STRING="Host=postgres:5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"

399
.gitignore vendored Normal file
View File

@ -0,0 +1,399 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.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

Binary file not shown.

35
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,35 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md.
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Cloud/bin/Debug/net6.0/Cloud.dll",
"args": [],
"cwd": "${workspaceFolder}/Cloud",
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"dotnet.defaultSolution": "Cloud.sln"
}

41
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Cloud.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/Cloud.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/Cloud.sln"
],
"problemMatcher": "$msCompile"
}
]
}

22
Cloud.sln Normal file
View File

@ -0,0 +1,22 @@

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}") = "Cloud", "Cloud\Cloud.csproj", "{D279AAA9-D4F8-4C7B-B34D-44A7429C87AA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D279AAA9-D4F8-4C7B-B34D-44A7429C87AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D279AAA9-D4F8-4C7B-B34D-44A7429C87AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D279AAA9-D4F8-4C7B-B34D-44A7429C87AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D279AAA9-D4F8-4C7B-B34D-44A7429C87AA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,31 @@
using Cloud.Models;
using Microsoft.EntityFrameworkCore;
namespace Cloud;
public class ApplicationContext : DbContext
{
public DbSet<User> Users { get; set; } = null!;
public DbSet<Farm> Farms { get; set; } = null!;
public DbSet<Greenhouse> Greenhouses { get; set; } = null!;
public ApplicationContext(DbContextOptions<ApplicationContext> options)
: base(options)
{
}
public ApplicationContext()
: base(new DbContextOptionsBuilder<ApplicationContext>()
.UseNpgsql("Host=localhost;Port=5438;Database=main_database;Username=postgres;Password=12345")
.Options)
{
Database.EnsureCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseNpgsql("Host=localhost;Port=5438;Database=main_database;Username=postgres;Password=12345");
}
}
}

24
Cloud/Cloud.csproj Normal file
View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Confluent.Kafka" Version="2.6.1" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.35.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,104 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Identity;
using Cloud.Models;
using Cloud.Requests;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
namespace Cloud.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private PasswordHasher<User> _passwordHasher;
private IConfiguration _config;
private ApplicationContext _context;
public AuthController(IConfiguration config, ApplicationContext context)
{
_passwordHasher = new PasswordHasher<User>();
_config = config;
_context = context;
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{
var existUser = await _context.Users.SingleOrDefaultAsync(u => u.Email == request.Email);
if (existUser != null) {
return BadRequest("Пользователь с такой эл. почтой уже существует");
}
var user = new User
{
Name = request.Name,
Email = request.Email,
Password = _passwordHasher.HashPassword(null, request.Password)
};
_context.Users.Add(user);
await _context.SaveChangesAsync();
return Ok("Пользователь успешно зарегистрирован");
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await _context.Users.SingleOrDefaultAsync(u => u.Email == request.Email);
if (user == null) {
return Unauthorized("Пользователя с такой эл. почтой не существует");
}
var verificationResult = _passwordHasher.VerifyHashedPassword(null, user.Password, request.Password);
if (verificationResult == PasswordVerificationResult.Failed) {
return Unauthorized("Неверный пароль");
}
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.Name, user.Email),
};
var Sectoken = new JwtSecurityToken(_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
claims: claims,
expires: DateTime.Now.AddMinutes(120),
signingCredentials: credentials);
var token = new JwtSecurityTokenHandler().WriteToken(Sectoken);
return Ok(token);
}
[Authorize]
[HttpGet("user")]
public async Task<IActionResult> GetAuthUser()
{
var userEmail = User.Identity.Name;
var user = await _context.Users.SingleOrDefaultAsync(u => u.Email == userEmail);
if (user == null) {
return NotFound("Пользователь не найден");
}
return Ok(new
{
user.Id,
user.Name,
user.Email
});
}
}

View File

@ -0,0 +1,128 @@
using Cloud.Models;
using Cloud.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Cloud.Controllers
{
[Authorize]
[ApiController]
[Route("api/user")]
public class FarmController : ControllerBase
{
private IConfiguration _config;
private ApplicationContext _context;
public FarmController(IConfiguration config, ApplicationContext context)
{
_config = config;
_context = context;
}
[HttpGet("{userId}/farm")]
public async Task<ActionResult<List<Farm>>> Index(int userId)
{
try
{
List<Farm> farms = await
_context.Farms.Where(x => x.UserId == userId).AsNoTracking().ToListAsync();
if (!farms.Any())
return NotFound("Farms is not found");
return Ok(farms);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpGet("{userId}/farm/{farmId}")]
public async Task<ActionResult<Farm>> Show(int userId, int farmId)
{
try
{
Farm? farm = await
_context.Farms.FirstOrDefaultAsync(x => x.UserId == userId && x.Id == farmId);
if (farm == null)
return NotFound("Farm is not found");
return Ok(farm);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpPost("{userId}/farm")]
public async Task<ActionResult<Farm>> Create([FromBody] FarmRequest farmRequest, int userId)
{
try
{
var farm = new Farm
{
Name = farmRequest.Name,
UserId = userId,
RaspberryIP = farmRequest.RaspberryIP,
};
Farm? farmCreated = _context.Farms.Add(farm).Entity;
await _context.SaveChangesAsync();
return Ok(farmCreated);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpPut("{userId}/farm/{farmId}")]
public async Task<ActionResult<Farm>> Update([FromBody] FarmRequest farmRequest, int userId, int farmId)
{
try
{
Farm? farm = await _context.Farms.FirstOrDefaultAsync(x => x.Id == farmId && x.UserId == userId);
if (farm == null)
return NotFound("Farm is not found");
farm.Name = farmRequest.Name;
farm.RaspberryIP = farmRequest.RaspberryIP;
_context.Farms.Update(farm);
await _context.SaveChangesAsync();
return Ok(farm);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpDelete("{userId}/farm/{farmId}")]
public async Task<ActionResult> Delete(int userId, int farmId)
{
try
{
Farm? farm = await _context.Farms.FirstOrDefaultAsync(x => x.Id == farmId && x.UserId == userId);
if (farm == null)
return NotFound("Farm is not found");
_context.Farms.Remove(farm);
await _context.SaveChangesAsync();
return Ok("Farm deleted successfully");
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
}

View File

@ -0,0 +1,160 @@
using Cloud.Models;
using Cloud.Requests;
using Cloud.Services.Broker;
using Cloud.Services.Broker.Support;
using Cloud.Services.Domain;
using Microsoft.AspNetCore.Mvc;
namespace Cloud.Controllers
{
[ApiController]
[Route("api/farm/{farmId}/greenhouse")]
public class GreenhouseController : ControllerBase
{
private readonly IGreenhouseService _greenhouseService;
public GreenhouseController(IGreenhouseService greenhouseService)
{
_greenhouseService = greenhouseService;
}
/// <summary>
/// Возвращает текущую информацию о всех теплицах пользователя
/// </summary>
/// <param name="farmId"></param>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult<List<GreenhouseInfo>>> GetAll(int farmId)
{
try
{
var greenhouses = _greenhouseService.GetAll(farmId);
if (greenhouses == null) return NotFound("Greenhouses is not found");
return Ok(greenhouses);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// Возвращает текущую информацию о конкретной теплице
/// </summary>
/// <param name="farmId"></param>
/// <param name="greenhouseId"></param>
/// <returns></returns>
[HttpGet("{greenhouseId}")]
public async Task<ActionResult<GreenhouseInfo>> Get(int farmId, int greenhouseId)
{
try
{
var greenhouses = _greenhouseService.GetGreenhouseInfo(greenhouseId, farmId);
return Ok(greenhouses);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// Возвращает сохраненные данные для автоматизации теплицы
/// </summary>
/// <param name="farmId"></param>
/// <param name="greenhouseId"></param>
/// <returns></returns>
[HttpGet("{greenhouseId}/settings")]
public async Task<ActionResult<Greenhouse>> GetGreenhouse(int farmId, int greenhouseId)
{
try
{
var greenhouse = await _greenhouseService.GetGreenhouse(greenhouseId);
if (greenhouse == null) return NotFound("Greenhouses is not found");
return Ok(greenhouse);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// Сохраняет в базе данных API данные для автоматизации теплицы
/// </summary>
/// <param name="farmId"></param>
/// <param name="greenhouse"></param>
/// <returns></returns>
[HttpPost]
public async Task<ActionResult<Greenhouse>> SaveToDatabase(int farmId, GreenhouseRequest greenhouse)
{
try
{
var greenhouseEntity = new Greenhouse()
{
RecomendedTemperature = greenhouse.RecomendedTemperature,
WateringMode = greenhouse.WateringMode,
HeatingMode = greenhouse.HeatingMode
};
var result = await _greenhouseService.Create(greenhouseEntity);
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// Обновляет в базе данных API данные для автоматизации теплицы
/// </summary>
/// <param name="farmId">ID фермы</param>
/// <param name="greenhouseId">ID теплицы</param>
/// <param name="greenhouse">Данные для обновления</param>
/// <returns>Обновленный объект Greenhouse</returns>
[HttpPut("{greenhouseId}/settings")]
public async Task<ActionResult<Greenhouse>> Update(int farmId, int greenhouseId, GreenhouseRequest greenhouse)
{
try
{
var greenhouseEntity = new Greenhouse()
{
Id = greenhouseId,
FarmId = farmId,
WateringMode = greenhouse.WateringMode,
HeatingMode = greenhouse.HeatingMode,
RecomendedTemperature = greenhouse.RecomendedTemperature
};
var result = await _greenhouseService.Update(greenhouseEntity);
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// Удаляет из базы данных API запись настроек автоматизации теплицы
/// </summary>
/// <param name="farmId"></param>
/// <param name="greenhouseId"></param>
/// <returns></returns>
[HttpDelete("{greenhouseId}")]
public async Task<ActionResult> Delete(int farmId, int greenhouseId)
{
try
{
_ = await _greenhouseService.Delete(greenhouseId);
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
}

View File

@ -0,0 +1,43 @@
using Cloud.Requests;
using Cloud.Services;
using Cloud.Services.Broker;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
namespace Cloud.Controllers
{
[Authorize]
[ApiController]
[Route("api")]
public class ValveController : ControllerBase
{
//Контроллер вентиля
private readonly IBrokerService _kafkaService;
public ValveController(IBrokerService kafkaService)
{
_kafkaService = kafkaService;
}
[HttpPost("farm/{farmId}/greenhouse/{ghId}/watering")]
public async Task<IActionResult> interactValve([FromBody] ValveRequest request, int farmId, int ghId)
{
var kafkaRequest = new
{
FarmId = farmId,
GreenHouseId = ghId,
SomeAction = request.Action,
};
var message = JsonSerializer.Serialize(kafkaRequest);
return Ok(kafkaRequest);
/*await _kafkaService.ProduceAsync("ValvesHeatersRequest", message);
return Ok($"Valve status is {request.Action}");*/
}
}
}

30
Cloud/Dockerfile Normal file
View File

@ -0,0 +1,30 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 5124
ENV ASPNETCORE_URLS=http://+:5124
# Creates a non-root user with an explicit UID and adds permission to access the /app folder
# For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
ARG configuration=Development
WORKDIR /src
COPY ["Cloud.csproj", "."]
RUN dotnet restore "./Cloud.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./Cloud.csproj" -c $configuration -o /app/build
FROM build AS publish
ARG configuration=Development
RUN dotnet publish "./Cloud.csproj" -c $configuration -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Cloud.dll"]

9
Cloud/Enums/ValveEnum.cs Normal file
View File

@ -0,0 +1,9 @@
namespace Cloud.Enums
{
public enum ValveEnum
{
Open,
Close,
Auto
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
namespace Cloud.Middlewares;
public static class DatabaseMiddleware
{
public static void AddDbConnectionService(this IServiceCollection services)
{
string connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING")
?? "Host=localhost;Port=5438;Database=main_database;Username=postgres;Password=12345";
services.AddDbContext<ApplicationContext>(options =>
options.UseNpgsql(connectionString));
}
public static void MigrateDb(this IApplicationBuilder app)
{
try
{
using var scope = app.ApplicationServices.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationContext>();
context.Database.Migrate();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}

View File

@ -0,0 +1,53 @@
// <auto-generated />
using Cloud;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Cloud.Migrations
{
[DbContext(typeof(ApplicationContext))]
[Migration("20241027220558_CreateUsersTable")]
partial class CreateUsersTable
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.14")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Cloud.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Cloud.Migrations
{
public partial class CreateUsersTable : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@ -0,0 +1,95 @@
// <auto-generated />
using Cloud;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Cloud.Migrations
{
[DbContext(typeof(ApplicationContext))]
[Migration("20241028192806_CreateFarmsTable")]
partial class CreateFarmsTable
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.14")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Cloud.Models.Farm", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RaspberryMacAddr")
.IsRequired()
.HasColumnType("text");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Farms");
});
modelBuilder.Entity("Cloud.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Cloud.Models.Farm", b =>
{
b.HasOne("Cloud.Models.User", "User")
.WithMany("Farms")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cloud.Models.User", b =>
{
b.Navigation("Farms");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Cloud.Migrations
{
public partial class CreateFarmsTable : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Farms",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: false),
RaspberryMacAddr = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Farms", x => x.Id);
table.ForeignKey(
name: "FK_Farms_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Farms_UserId",
table: "Farms",
column: "UserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Farms");
}
}
}

View File

@ -0,0 +1,95 @@
// <auto-generated />
using Cloud;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Cloud.Migrations
{
[DbContext(typeof(ApplicationContext))]
[Migration("20241030111034_front")]
partial class front
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.14")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Cloud.Models.Farm", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RaspberryMacAddr")
.IsRequired()
.HasColumnType("text");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Farms");
});
modelBuilder.Entity("Cloud.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Cloud.Models.Farm", b =>
{
b.HasOne("Cloud.Models.User", "User")
.WithMany("Farms")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cloud.Models.User", b =>
{
b.Navigation("Farms");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Cloud.Migrations
{
public partial class front : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -0,0 +1,93 @@
// <auto-generated />
using Cloud;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Cloud.Migrations
{
[DbContext(typeof(ApplicationContext))]
partial class ApplicationContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.14")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Cloud.Models.Farm", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RaspberryMacAddr")
.IsRequired()
.HasColumnType("text");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Farms");
});
modelBuilder.Entity("Cloud.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Cloud.Models.Farm", b =>
{
b.HasOne("Cloud.Models.User", "User")
.WithMany("Farms")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cloud.Models.User", b =>
{
b.Navigation("Farms");
});
#pragma warning restore 612, 618
}
}
}

12
Cloud/Models/Farm.cs Normal file
View File

@ -0,0 +1,12 @@
namespace Cloud.Models
{
public class Farm
{
public int Id { get; set; }
public string Name { get; set; }
public int UserId { get; set; }
public User? User { get; set; }
public string RaspberryIP { get; set; }
List<Greenhouse> Greenhouses { get; set; } = new();
}
}

View File

@ -0,0 +1,14 @@
using Cloud.Models.Support;
namespace Cloud.Models
{
public class Greenhouse
{
public int Id { get; set; }
public int RecomendedTemperature { get; set; }
public WateringMode WateringMode { get; set; }
public HeatingMode HeatingMode { get; set; }
public int FarmId { get; set; }
public Farm? Farm { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace Cloud.Models.Support
{
public enum HeatingMode
{
Manual,
Auto
}
}

View File

@ -0,0 +1,8 @@
namespace Cloud.Models.Support
{
public enum WateringMode
{
Manual,
Auto
}
}

13
Cloud/Models/User.cs Normal file
View File

@ -0,0 +1,13 @@
namespace Cloud.Models;
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public List<Farm> Farms { get; set; } = new();
}

133
Cloud/Program.cs Normal file
View File

@ -0,0 +1,133 @@
using Cloud;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using FluentValidation;
using FluentValidation.AspNetCore;
using Cloud.Validation;
using StackExchange.Redis;
using Cloud.Services.Broker.Implement.Kafka;
using Cloud.Services.Broker;
using Cloud.Services;
using Cloud.Services.Domain.Implement;
using Cloud.Services.Domain;
using Cloud.Services.Cache;
using Cloud.Support;
using System.Text.RegularExpressions;
using Cloud.Middlewares;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddSingleton<IBrokerService, KafkaService>();
builder.Services.AddTransient<IGreenhouseService, GreenhouseService>();
//Redis configuration
string redisUrl = Environment.GetEnvironmentVariable("REDIS_URL") ?? "localhost:6379";
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var configuration = ConfigurationOptions.Parse(redisUrl);
return ConnectionMultiplexer.Connect(configuration);
});
builder.Services.AddSingleton<IRedisCacheService, RedisCacheService>();
//Jwt configuration
var jwtIssuer = builder.Configuration.GetSection("Jwt:Issuer").Get<string>();
var jwtKey = builder.Configuration.GetSection("Jwt:Key").Get<string>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtIssuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
});
// Настройка подключения к БД
builder.Services.AddDbConnectionService();
// Настройка CORS
string frontUrl = Environment.GetEnvironmentVariable("FRONT_URL") ?? "http://localhost:3000";
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontendLocalhost", builder =>
{
builder.WithOrigins(frontUrl) // фронтенд
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddControllers();
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddFluentValidationClientsideAdapters();
builder.Services.AddValidatorsFromAssemblyContaining<LoginValidator>();
builder.Services.AddValidatorsFromAssemblyContaining<RegisterValidator>();
builder.Services.AddValidatorsFromAssemblyContaining<FarmValidator>();
builder.Services.AddValidatorsFromAssemblyContaining<ValveValidator>();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "Cloud API", Version = "v1" });
c.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description = "Введите ваш Bearer токен",
Name = "Authorization",
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
{
{
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new Microsoft.OpenApi.Models.OpenApiReference
{
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
Console.WriteLine("Swagger enabled");
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Cloud API V1");
c.RoutePrefix = string.Empty;
});
}
app.UseHttpsRedirection();
// Включение CORS
app.UseCors("AllowFrontendLocalhost");
// Применение миграций
app.MigrateDb();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:43967",
"sslPort": 44304
}
},
"profiles": {
"Cloud": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7113;http://localhost:5124",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,8 @@
namespace Cloud.Requests
{
public class FarmRequest
{
public string Name { get; set; }
public string RaspberryIP { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using Cloud.Models.Support;
namespace Cloud.Requests
{
public class GreenhouseRequest
{
public int RecomendedTemperature { get; set; }
public WateringMode WateringMode { get; set; }
public HeatingMode HeatingMode { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Cloud.Requests;
public class LoginRequest
{
public string Email { get; set; }
public string Password { get; set; }
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Cloud.Requests;
public class RegisterRequest
{
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace Cloud.Requests
{
public class ValveRequest
{
public string Action { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using Cloud.Services.Broker.Support;
namespace Cloud.Services.Broker
{
public interface IBrokerConsumer
{
IEnumerable<T>? WaitMessages<T>(string topic) where T : IBrokerResponse;
}
}

View File

@ -0,0 +1,9 @@
using Cloud.Services.Broker.Support;
namespace Cloud.Services.Broker
{
public interface IBrokerProducer
{
Task ProduceAsync(string topic, Command command);
}
}

View File

@ -0,0 +1,9 @@
using Cloud.Services.Broker.Support;
namespace Cloud.Services.Broker
{
public interface IBrokerService : IBrokerProducer, IBrokerConsumer
{
void ChangeBrokerIp(string ip);
}
}

View File

@ -0,0 +1,93 @@
using Cloud.Services.Broker.Support;
using Confluent.Kafka;
using System.Diagnostics;
using System.Text.Json;
namespace Cloud.Services.Broker.Implement.Kafka
{
public class KafkaConsumer : IBrokerConsumer
{
private IConsumer<string, string> _consumer;
private readonly IConfiguration _config;
public KafkaConsumer(IConfiguration config)
{
_config = config;
Console.WriteLine($"KafkaConsumer created. IP:" + _config["KAFKA_URL"]);
ChangeBrokerIp(_config["KAFKA_URL"]);
}
public IEnumerable<T>? WaitMessages<T>(string topic)
where T : IBrokerResponse
{
List<T> res = new();
List<PartitionMetadata> partitions;
using var adminClient = new AdminClientBuilder(new AdminClientConfig { BootstrapServers = _config["KAFKA_URL"] }).Build();
var meta = adminClient.GetMetadata(TimeSpan.FromSeconds(20));
var currentTopic = meta.Topics.SingleOrDefault(t => t.Topic == topic)
?? throw new Exception($"Topic {topic} not found");
partitions = currentTopic.Partitions;
_consumer.Subscribe(topic);
foreach (var partition in partitions)
{
var topicPartition = new TopicPartition(topic, partition.PartitionId);
_consumer.Assign(topicPartition);
T? message = _consume<T>();
if (message == null) return null;
res.Add(message);
}
_consumer.Unassign();
_consumer.Unsubscribe();
return res;
}
private T? _consume<T>() where T : IBrokerResponse
{
var sw = new Stopwatch();
sw.Start();
try
{
while (true)
{
var consumeResult = _consumer.Consume(TimeSpan.FromMinutes(1));
if (consumeResult?.Message?.Value == null)
{
// Предел по времени
if (sw.Elapsed > TimeSpan.FromMinutes(1))
{
return default;
}
continue;
}
string jsonObj = consumeResult.Message.Value;
return JsonSerializer.Deserialize<T>(jsonObj);
}
}
catch (Exception ex)
{
_consumer.Close();
throw;
}
}
public void ChangeBrokerIp(string ip)
{
var consumerConfig = new ConsumerConfig()
{
BootstrapServers = ip,
GroupId = _config["Kafka:GroupId"],
AutoOffsetReset = AutoOffsetReset.Earliest,
};
_consumer?.Close();
_consumer = new ConsumerBuilder<string, string>(consumerConfig).Build();
}
}
}

View File

@ -0,0 +1,38 @@
using System.Text.Json;
using Cloud.Services.Broker.Support;
using Confluent.Kafka;
namespace Cloud.Services.Broker.Implement.Kafka
{
public class KafkaProducer : IBrokerProducer
{
private IProducer<string, string> _producer;
private readonly IConfiguration _config;
public KafkaProducer(IConfiguration configuration)
{
_config = configuration;
Console.WriteLine($"KafkaProducer created. IP:" + _config["KAFKA_URL"]);
ChangeBrokerIp(_config["KAFKA_URL"]);
}
public async Task ProduceAsync(string topic, Command command)
{
var commandSerialized = JsonSerializer.Serialize(command);
var message = new Message<string, string> { Key = Guid.NewGuid().ToString(), Value = commandSerialized };
//Produce the Message
await _producer.ProduceAsync(topic, message);
}
public void ChangeBrokerIp(string ip)
{
var producerConfig = new ProducerConfig
{
BootstrapServers = ip
};
//Build the Producer
_producer = new ProducerBuilder<string, string>(producerConfig).Build();
}
}
}

View File

@ -0,0 +1,30 @@
using Cloud.Services.Broker.Support;
namespace Cloud.Services.Broker.Implement.Kafka
{
public class KafkaService : IBrokerService
{
private readonly KafkaProducer _producer;
private readonly KafkaConsumer _consumer;
public KafkaService(IConfiguration configuration)
{
_producer = new KafkaProducer(configuration);
_consumer = new KafkaConsumer(configuration);
}
public IEnumerable<T>? WaitMessages<T>(string topic)
where T : IBrokerResponse
=> _consumer.WaitMessages<T>(topic);
public async Task ProduceAsync(string topic, Command command)
=> await _producer.ProduceAsync("commands", command);
public void ChangeBrokerIp(string ip)
{
_consumer.ChangeBrokerIp(ip);
_producer.ChangeBrokerIp(ip);
}
}
}

View File

@ -0,0 +1,10 @@
using System.Text.Json;
namespace Cloud.Services.Broker.Support
{
public class Command
{
public Guid GreenhouseId { get; set; }
public string CommandName { get; set; } = null!;
}
}

View File

@ -0,0 +1,9 @@
namespace Cloud.Services.Broker.Support
{
public class CommandResult : IBrokerResponse
{
public int CommandId { get; set; }
public int GreenhouseId { get; set; }
public string ResultMessage { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,12 @@
namespace Cloud.Services.Broker.Support
{
public class GreenhouseInfo : IBrokerResponse
{
public int Id { get; set; }
public int PercentWater { get; set; }
public int SoilTemperature { get; set; }
public bool PumpStatus { get; set; }
public bool HeatingStatus { get; set; }
public bool AutoWateringStatus { get; set; }
}
}

View File

@ -0,0 +1,6 @@
namespace Cloud.Services.Broker.Support
{
public interface IBrokerResponse
{
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Cloud.Services.Cache
{
public interface IRedisCacheService
{
Task SetCacheAsync<T>(string key, T value, TimeSpan? expiry = null);
Task<T?> GetCacheAsync<T>(string key);
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using StackExchange.Redis;
using System.Text.Json;
namespace Cloud.Services.Cache
{
public class RedisCacheService : IRedisCacheService
{
private readonly IConnectionMultiplexer _connectionMultiplexer;
public RedisCacheService(IConnectionMultiplexer connectionMultiplexer)
{
_connectionMultiplexer = connectionMultiplexer;
}
public async Task SetCacheAsync<T>(string key, T value, TimeSpan? expiry = null)
{
var database = _connectionMultiplexer.GetDatabase();
var serializedValue = JsonSerializer.Serialize(value);
await database.StringSetAsync(key, serializedValue, expiry);
}
public async Task<T?> GetCacheAsync<T>(string key)
{
var database = _connectionMultiplexer.GetDatabase();
var value = await database.StringGetAsync(key);
if (value.IsNullOrEmpty)
return default;
return JsonSerializer.Deserialize<T>(value);
}
}
}

View File

@ -0,0 +1,45 @@
using Cloud.Models;
using Cloud.Services.Broker.Support;
namespace Cloud.Services.Domain;
public interface IGreenhouseService
{
/// <summary>
/// Возвращает текущую информацию о конкретной теплице из брокера
/// </summary>
/// <param name="id">ID теплицы</param>
/// <param name="farmId">ID фермы, то есть брокера</param>
/// <returns>Текущие данные о теплице от менеджера теплицы</returns>
public Task<GreenhouseInfo?> GetGreenhouseInfo(int id, int farmId);
/// <summary>
/// Возвращает сохраненные данные для автоматизации теплицы из базы данных
/// </summary>
/// <param name="id">ID теплицы</param>
/// <returns>Данные для автоматизации теплицы</returns>
public Task<Greenhouse?> GetGreenhouse(int id);
/// <summary>
/// Возвращает список данных о всех теплицах пользователя из брокера
/// </summary>
/// <param name="farmId">ID фермы</param>
/// <returns>Список текущих данных о теплицах</returns>
public Task<IEnumerable<GreenhouseInfo>?> GetAll(int farmId);
/// <summary>
/// Сохраняет данные об автоматизации теплицы в базу данных
/// </summary>
/// <param name="greenhouse">Данные автоматизации теплицы</param>
/// <returns>Созданную сущность из базы данных</returns>
public Task<Greenhouse> Create(Greenhouse greenhouse);
/// <summary>
/// Обновляет данные автоматизации теплицы в базе данных
/// </summary>
/// <param name="greenhouse">Новая информация об автоматизации теплицы</param>
/// <returns>Обновленную сущность из базы данных</returns>
public Task<Greenhouse> Update(Greenhouse greenhouse);
/// <summary>
/// Удаляет данные об автоматизации теплицы из базы данных
/// </summary>
/// <param name="id">ID данных автоматизации теплицы</param>
/// <returns>Возвращает удаленную сущность</returns>
public Task<Greenhouse> Delete(int id);
}

View File

@ -0,0 +1,67 @@
using Cloud.Models;
using Cloud.Services.Broker;
using Cloud.Services.Broker.Support;
using Microsoft.EntityFrameworkCore;
namespace Cloud.Services.Domain.Implement;
public class GreenhouseService : IGreenhouseService
{
private readonly IBrokerService _brokerService;
private readonly ApplicationContext _context;
public GreenhouseService(IBrokerService brokerService, ApplicationContext context)
{
_context = context;
_brokerService = brokerService;
}
public async Task<Greenhouse> Create(Greenhouse greenhouse)
{
var res = await _context.Greenhouses.AddAsync(greenhouse);
await _context.SaveChangesAsync();
return res.Entity;
}
public async Task<Greenhouse> Delete(int id)
{
var greenhouse = await _context.Greenhouses.FirstOrDefaultAsync(x => x.Id == id);
_context.Greenhouses.Remove(greenhouse);
await _context.SaveChangesAsync();
return greenhouse;
}
public async Task<Greenhouse?> GetGreenhouse(int id)
{
return await _context.Greenhouses.FirstOrDefaultAsync(x => x.Id == id);
}
public async Task<Greenhouse> Update(Greenhouse greenhouse)
{
var res = _context.Greenhouses.Update(greenhouse);
await _context.SaveChangesAsync();
return res.Entity;
}
public async Task<IEnumerable<GreenhouseInfo>?> GetAll(int farmId)
{
// await _changeBrokerIp(farmId);
return _brokerService.WaitMessages<GreenhouseInfo>("data");
}
public async Task<GreenhouseInfo?> GetGreenhouseInfo(int id, int farmId)
{
// await _changeBrokerIp(farmId);
var infos = _brokerService.WaitMessages<GreenhouseInfo>("data");
return infos?.FirstOrDefault(x => x.Id == id);
}
private async Task _changeBrokerIp(int farmId)
{
var farm = await _context.Farms.FirstOrDefaultAsync(x => x.Id == farmId);
_brokerService.ChangeBrokerIp(farm.RaspberryIP);
}
}

View File

@ -0,0 +1,26 @@
namespace Cloud.Support;
public static class NetworkSupport
{
public static async Task CheckConnectionAsync(string address)
{
using var client = new HttpClient();
try
{
var response = await client.GetAsync(address);
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"Соединение успешно проверено. Статус-код: {response.StatusCode}");
}
else
{
Console.WriteLine($"Соединение не удалось проверить. Статус-код: {response.StatusCode}. URL: {address}");
}
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Ошибка при проверке соединения: {ex.Message}. URL: {address}");
}
}
}

View File

@ -0,0 +1,18 @@
using Cloud.Requests;
using FluentValidation;
namespace Cloud.Validation
{
public class FarmValidator : AbstractValidator<FarmRequest>
{
public FarmValidator()
{
RuleFor(request => request.RaspberryIP)
.NotEmpty().WithMessage("IP address can't be empty")
.Matches(@"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$").WithMessage("IP address is not valid");
RuleFor(request => request.Name)
.NotEmpty().WithMessage("Name can't be empty");
}
}
}

View File

@ -0,0 +1,18 @@
using Cloud.Requests;
using FluentValidation;
namespace Cloud.Validation;
public class LoginValidator : AbstractValidator<LoginRequest>
{
public LoginValidator()
{
RuleFor(request => request.Email)
.NotEmpty().WithMessage("Email обязателен для заполнения")
.EmailAddress().WithMessage("Некорректный формат Email");
RuleFor(request => request.Password)
.NotEmpty().WithMessage("Пароль обязателен для заполнения")
.MinimumLength(8).WithMessage("Пароль должен быть не менее 8 символов");
}
}

View File

@ -0,0 +1,22 @@
using Cloud.Requests;
using FluentValidation;
namespace Cloud.Validation;
public class RegisterValidator : AbstractValidator<RegisterRequest>
{
public RegisterValidator()
{
RuleFor(user => user.Name)
.NotEmpty().WithMessage("Имя обязательно для заполнения")
.MaximumLength(50).WithMessage("Имя должно быть не более 50 символов");
RuleFor(user => user.Email)
.NotEmpty().WithMessage("Email обязателен для заполнения")
.EmailAddress().WithMessage("Некорректный формат Email");
RuleFor(user => user.Password)
.NotEmpty().WithMessage("Пароль обязателен для заполнения")
.MinimumLength(8).WithMessage("Пароль должен быть не менее 8 символов");
}
}

View File

@ -0,0 +1,16 @@
using Cloud.Enums;
using Cloud.Requests;
using FluentValidation;
namespace Cloud.Validation
{
public class ValveValidator : AbstractValidator<ValveRequest>
{
public ValveValidator() {
RuleFor(request => request.Action)
.NotEmpty().WithMessage("Action can't be empty").
IsEnumName(typeof (ValveEnum)).WithMessage("Action is not correct");
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

17
Cloud/appsettings.json Normal file
View File

@ -0,0 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Kafka": {
"BootstrapServers": "localhost:9092",
"GroupId": "ValvesHeaters"
},
"AllowedHosts": "*",
"Jwt": {
"Key": "m7TyhE20s0dVtUDAr9EnFdPZnAG8maxgBTaiW5j6kO6RQhWDAGxYmXyu0suDnE0o",
"Issuer": "localhost"
}
}

View File

@ -1,4 +0,0 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]

View File

@ -1,23 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// Этот код создан программой.
// Исполняемая версия:4.0.30319.42000
//
// Изменения в этом файле могут привести к неправильной работе и будут потеряны в случае
// повторной генерации кода.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("CucumberCloud")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("CucumberCloud")]
[assembly: System.Reflection.AssemblyTitleAttribute("CucumberCloud")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Создано классом WriteCodeFragment MSBuild.

View File

@ -1 +0,0 @@
87ee1ebed43dd90dbbceb9a51a9dd6b4d21d9cc6

View File

@ -1,17 +0,0 @@
is_global = true
build_property.TargetFramework = net6.0
build_property.TargetPlatformMinVersion =
build_property.UsingMicrosoftNETSdkWeb = true
build_property.ProjectTypeGuids =
build_property.InvariantGlobalization =
build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = CucumberCloud
build_property.RootNamespace = CucumberCloud
build_property.ProjectDir = C:\Users\User\source\repos\CucumberApp\CucumberCloud\
build_property.RazorLangVersion = 6.0
build_property.SupportLocalizedComponentNames =
build_property.GenerateRazorMetadataSourceChecksumAttributes =
build_property.MSBuildProjectDirectory = C:\Users\User\source\repos\CucumberApp\CucumberCloud
build_property._RazorSourceGeneratorDebug =

View File

@ -1,17 +0,0 @@
// <auto-generated/>
global using global::Microsoft.AspNetCore.Builder;
global using global::Microsoft.AspNetCore.Hosting;
global using global::Microsoft.AspNetCore.Http;
global using global::Microsoft.AspNetCore.Routing;
global using global::Microsoft.Extensions.Configuration;
global using global::Microsoft.Extensions.DependencyInjection;
global using global::Microsoft.Extensions.Hosting;
global using global::Microsoft.Extensions.Logging;
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Net.Http.Json;
global using global::System.Threading;
global using global::System.Threading.Tasks;

View File

@ -1,68 +0,0 @@
services:
postgres:
image: postgres:14
container_name: cucumer_database
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 12345
POSTGRES_DB: main_database
ports:
- "5438:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7
container_name: my_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
zookeeper:
image: confluentinc/cp-zookeeper:7.4.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- 2181:2181
kafka:
image: confluentinc/cp-kafka:7.4.0
ports:
- 9092:9092
expose:
- 29092:29092
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_LOG_FLUSH_INTERVAL_MESSAGES: 10000
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
depends_on:
- zookeeper
init-kafka:
image: confluentinc/cp-kafka:7.4.0
depends_on:
- kafka
entrypoint: [ '/bin/sh', '-c' ]
command: |
"
# blocks until kafka is reachable
kafka-topics --bootstrap-server kafka:29092 --list
echo -e 'Creating kafka topics'
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic commands --replication-factor 1 --partitions 1
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic data --replication-factor 1 --partitions 1
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic response --replication-factor 1 --partitions 1
echo -e 'Successfully created the following topics:'
kafka-topics --bootstrap-server kafka:29092 --list
"

4
Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM docker/whalesay:latest
LABEL Name=cucumber Version=0.0.1
RUN apt-get -y update && apt-get install -y fortunes
CMD ["sh", "-c", "/usr/games/fortune -a | cowsay"]

View File

@ -0,0 +1,48 @@
from json import dumps
from json import dumps
class ManageController:
def __init__(self, producer, topic='commands'):
self.valve_state = "closed"
self.heater_state = "off"
self.producer = producer
self.topic = topic
def toggle_device(self, device, request_id, greenhouse_id):
if device == 'valve':
if self.valve_state == 'closed':
self.valve_state = 'open'
print("Valve opened")
else:
self.valve_state = 'closed'
print("Valve closed")
elif device == 'heater':
if self.heater_state == 'off':
self.heater_state = 'on'
print("Heater turned on")
else:
self.heater_state = 'off'
print("Heater turned off")
self.send_status(request_id, greenhouse_id)
def send_status(self, request_id, greenhouse_id):
status = {
'request_id': request_id,
'greenhouse_id': greenhouse_id,
'valve_state': self.valve_state,
'heater_state': self.heater_state
}
print(f"Sent device status: {status}")
return status

View File

@ -0,0 +1,11 @@
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY GreenhouseDetector/detector.py .
CMD ["python", "detector.py"]

View File

@ -0,0 +1,43 @@
import os
import time
import random as rnd
from flask import Flask
import requests
import threading
app = Flask(__name__)
class Detector:
def __init__(self, id, moistureThresholdUpper, moistureThresholdLower, tempThresholdUpper, tempThresholdLower):
self.MANAGER_URL = os.environ.get('MANAGER_URL')
print("MANAGER_URL=", self.MANAGER_URL)
self.id = id
self.moistureThresholdUpper = moistureThresholdUpper
self.moistureThresholdLower = moistureThresholdLower
self.tempThresholdUpper = tempThresholdUpper
self.tempThresholdLower = tempThresholdLower
self.moisture = 0
self.temp = 0
def cycle(self):
self.moisture += rnd.random() / 100
self.temp += (rnd.random() - 0.5) / 100
def sendData(self):
data = {"moisture": self.moisture,
"temp": self.temp}
requests.post(f"{self.MANAGER_URL}/webhook?id={self.id}", json=data)
detector1 = Detector(1, 0.6, 0.2, 40, 20)
detectors = [detector1]
if __name__ =="__main__":
while True:
for detector in detectors:
detector.cycle()
detector.sendData()
time.sleep(1)

View File

@ -0,0 +1,11 @@
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY GreenhouseManager/manager.py .
CMD ["python", "manager.py"]

View File

@ -0,0 +1,140 @@
import os
from kafka import KafkaProducer, KafkaConsumer
import kafka
import socket
from json import dumps, loads
from flask import Flask, request
import time
from enum import Enum
import threading
app = Flask(__name__)
def start_manager():
return
class Manager:
def __init__(self, _id: int, moisture: float = 0, temp: float = 20, isAutoOn: bool = False, valve_state: str = "closed",
heater_state: str = "off"):
KAFKA_URL = os.environ.get('KAFKA_URL')
print("KAFKA_URL=", KAFKA_URL)
self._id = _id
self.moisture = moisture
self.temp = temp
self.isAutoOn = isAutoOn
self.valve_state = valve_state
self.heater_state = heater_state
self.dataPublisher = KafkaProducer(
bootstrap_servers=[KAFKA_URL],
client_id=f'manager{self._id}_producer',
value_serializer=lambda v: dumps(v).encode('utf-8')
)
self.controllerConsumer = KafkaConsumer(
'commands',
bootstrap_servers=[KAFKA_URL],
auto_offset_reset='earliest',
enable_auto_commit=True,
consumer_timeout_ms=2000,
group_id=f'manager{self._id}',
value_deserializer=lambda x: loads(x.decode('utf-8'))
)
self.controllerConsumerResponse = KafkaProducer(
bootstrap_servers=[KAFKA_URL],
client_id=f'manager{self._id}_producer',
value_serializer=lambda v: dumps(v).encode('utf-8')
)
def sendData(self):
print("sending data...")
message = {
'id': self._id,
'moisture': self.moisture,
'temp': self.temp,
'valveStatus': str(self.valve_state),
'heaterStatus': str(self.heater_state),
'isAutoOn': self.isAutoOn
}
print(message)
self.dataPublisher.send('data', message)
self.dataPublisher.flush()
def toggle_device(self, device, request_id, greenhouse_id):
if device == 'valve':
if self.valve_state == 'closed':
self.valve_state = 'open'
print("Valve opened")
else:
self.valve_state = 'closed'
print("Valve closed")
elif device == 'heater':
if self.heater_state == 'off':
self.heater_state = 'on'
print("Heater turned on")
else:
self.heater_state = 'off'
print("Heater turned off")
self.send_status(request_id, greenhouse_id)
def send_status(self, request_id, greenhouse_id):
status = {
'request_id': request_id,
'greenhouse_id': greenhouse_id,
'valve_state': self.valve_state,
'heater_state': self.heater_state
}
self.sendDataCommand(status)
print("Updating info...\n")
def sendDataCommand(self, message):
print("sending data...")
self.dataPublisher.send('response', message)
def getCommand(self):
messages = self.controllerConsumer.poll(timeout_ms=1000)
# Проверяем, есть ли сообщения
for tp, msgs in messages.items():
for message in msgs:
print(f"Manager {self._id} received message: ")
print(message.value)
self.request_id = message.value['request_id']
self.greenhouse_id = message.value['greenhouse_id']
self.command = message.value['command']
self.toggle_device(self.command, self.request_id, self.greenhouse_id)
@app.route(f'/webhook', methods=['POST'])
def webhook():
print("received webhook", request.args.get('id'))
for manager in managers:
print()
if int(request.args.get('id')) == manager._id and request.method == 'POST':
print("Data received from Webhook is", request.json)
body = request.json
for key, value in body.items():
setattr(manager, key, value)
manager.sendData()
return f"Webhook received for manager {manager._id}"
return "Webhook ignored"
t1 = threading.Thread(target=start_manager)
manager1 = Manager(_id=1)
managers = [manager1]
if __name__ == "__main__":
threading.Thread(target=lambda: app.run(host="0.0.0.0", port=20002, debug=True, use_reloader=False)).start()

24
cucumber-frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/.pnp
.pnp.js
node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,46 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

31730
cucumber-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
{
"name": "cucumber-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.5.1",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.115",
"@types/react": "^18.3.12",
"antd": "^5.21.6",
"axios": "^1.7.7",
"chart.js": "^4.4.7",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.3.5",
"react-router-dom": "^6.27.0",
"react-scripts": "^5.0.1",
"swagger-typescript-api": "^13.0.23",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"gen-api": "swagger-typescript-api -r -o ./src/core/api/ --modular -p "
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react-dom": "^18.3.1",
"tailwindcss": "^3.4.14"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,62 @@
import axios, { AxiosError, AxiosRequestHeaders } from 'axios';
import IUser from '../models/IUser';
import IFarm from '../models/IFarm';
import LoginRequest from '../Requests/LoginRequest';
import IRegisterRequest from '../Requests/RegisterRequest';
const API_BASE_URL = 'http://localhost:5124/api';
const getHeaders = (): { [key: string]: string } => {
return {
'Content-Type': 'application/json',
};
};
export const getFarms = async (): Promise<IFarm[]> => {
try {
const response = await axios.get(`${API_BASE_URL}/farms`, { headers: getHeaders() });
return response.data;
} catch (error: unknown) {
const axiosError = error as AxiosError;
console.error('Error fetching farms:', axiosError.message);
throw error;
}
};
export const getUser = async (userId: number): Promise<IUser> => {
try {
const response = await axios.get(`${API_BASE_URL}/Auth/user/${userId}`, { headers: getHeaders() });
return response.data;
} catch (error: unknown) {
const axiosError = error as AxiosError;
console.error('Error fetching user:', axiosError.message);
throw error;
}
};
export const createUser = async (userData: IRegisterRequest): Promise<IUser> => {
try {
const response = await axios.post(`${API_BASE_URL}/Auth/register`, userData, { headers: getHeaders() });
return response.data;
} catch (error: unknown) {
const axiosError = error as AxiosError;
console.error('Error creating user:', axiosError.message);
if (axiosError.response) {
// Логируем дополнительные данные об ошибке
console.error('Response data:', axiosError.response.data);
console.error('Response status:', axiosError.response.status);
}
throw error;
}
};
export const loginUser = async (userData: LoginRequest): Promise<string> => {
try {
const response = await axios.post(`${API_BASE_URL}/Auth/login`, userData, { headers: getHeaders() });
return response.data; // JWT токен
} catch (error: unknown) {
const axiosError = error as AxiosError;
console.error('Error logging in:', axiosError.message);
throw error;
}
};

View File

@ -0,0 +1,44 @@
.App {
text-align: center;
}
#main-content {
display: flex;
flex-direction: row;
min-height: 100vh;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -0,0 +1,33 @@
import React from 'react';
import './App.css';
import { Navigate, Route, Routes } from 'react-router-dom';
import {LoginPage} from './pages/Login';
import {RegisterPage} from './pages/Register';
import {AppLayout} from './components/Layout';
import { ProfilePage } from './pages/Profile';
import { GreenHouseListPage } from './pages/GreenHouseListPage';
import { ReportPage } from './pages/ReportPage';
import GreenHousePage from './pages/GreenHousePage';
import {GreenHouseStats} from './components/greenhouse/GreenHouseStats';
function App() {
return (
<div className="App">
<Routes>
<Route path="/" element={<AppLayout />}>
<Route index element={<Navigate to="/report" />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/greenhouses" element={<GreenHouseListPage />} />
<Route path="/greenhouses/:id" element={<GreenHousePage />} />
<Route path="/greenhouses/:id/statistics" element={<GreenHouseStats />} />
<Route path="/report" element={<ReportPage />} />
</Route>
</Routes>
</div>
);
}
export default App;

View File

@ -0,0 +1,6 @@
interface ILoginRequest {
email: string;
password: string;
}
export default ILoginRequest;

View File

@ -0,0 +1,7 @@
interface IRegisterRequest {
name: string;
email: string;
password: string;
}
export default IRegisterRequest;

View File

@ -0,0 +1,8 @@
export function Footer () {
return (
<footer className="footer">
<p>&copy; {new Date().getFullYear()} Cucumber</p>
</footer>
);
};

View File

@ -0,0 +1,27 @@
import React from 'react';
import { Link } from 'react-router-dom';
import Title from 'antd/es/typography/Title';
import { HomeOutlined, LoginOutlined, ProfileOutlined } from '@ant-design/icons';
export function Header () {
return (
<div className="flex items-center justify-between bg-slate-700" style={{ height: '100px'}}>
<div className="logo">
<Title level={1} className='text-white' style={{ marginLeft: '20px' }}><Link to="/" style={{ textDecoration: 'none', color: 'white'}}>Cucumber</Link></Title>
</div>
<nav className="nav">
<ul className="flex">
<li className="mr-4">
<Link to="/login"><LoginOutlined style={{ fontSize: '24px' }} className="text-white"/></Link>
</li>
<li>
<Link to="/profile"><ProfileOutlined style={{ fontSize: '24px', marginRight: '20px'}} className="text-white"/></Link>
</li>
<li>
<Link to="/greenhouses"><HomeOutlined style={{ fontSize: '24px', marginRight: '20px'}} className="text-white"/> </Link>
</li>
</ul>
</nav>
</div>
);
};

View File

@ -0,0 +1,19 @@
import React from 'react';
import { Layout } from 'antd';
import { Outlet } from 'react-router-dom';
import {Header} from './Header';
import {Footer} from './Footer';
export function AppLayout () {
return (
<div id="app-layout" className="bg-slate-100">
<Header />
<div id="main-content">
<Outlet />
</div>
<Footer />
</div>
);
};
export default AppLayout;

View File

@ -0,0 +1,187 @@
import React, { useState, useEffect } from 'react';
import { DatePicker, Select, Button, Form } from 'antd';
import { useParams } from 'react-router-dom';
import { Line } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
ArcElement,
PointElement,
LineElement,
} from 'chart.js';
import GreenhouseService from '../../core/services/greenhouse-service';
import { setLabels } from 'react-chartjs-2/dist/utils';
export function GreenHouseStats() {
const { id } = useParams<{ id: string }>();
const [from, setFrom] = useState<Date | null>(null);
const [to, setTo] = useState<Date | null>(null);
const [type, setType] = useState<'humidity' | 'temperature'>('humidity');
const service = new GreenhouseService();
const temperatureHistory = service.getTemperatureHistory(1);
const humidityHistory = service.getHumidityHistory(1);
const isButtonDisabled = !from || !to || (to.valueOf() === undefined || from.valueOf() === undefined)
//const isButtonDisabled = !from || !to || (to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24) > 30;
ChartJS.register(ArcElement, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
const [chartData, setChartData] = useState({
labels: [],
datasets: [
{
label: '',
data: [],
borderColor: '',
fill: false,
tension: 0.4,
},
],
});
const [data, setData] = useState({
labels: ["Jan", "Feb", "Mar", "Apr", "May"],
datasets: [
{
label: "Up Line",
data: [10, 20, 15, 40, 30],
borderColor: "rgb(28, 44, 44)", // Customize line color
fill: false,
tension: 0.4, // Adjust line tension for smoother curves
},
],
});
useEffect(() => {
}, [from, to, type]);
const createDateRange = (startDate: string, endDate: string) => {
const start = new Date(startDate);
const end = new Date(endDate);
const dates = [];
let currentDate = start;
while (currentDate <= end) {
dates.push(new Date(currentDate));
currentDate.setDate(currentDate.getDate() + 1);
}
return dates;
};
const handleGenerateReport = () => {
console.log(from, to, type);
if (from && to) {
const startDate = '2024-12-14';
const endDate = '2024-12-24';
const itemdata = type === 'humidity' ? humidityHistory : temperatureHistory;
const startDateObj = new Date(Date.parse(startDate));
const endDateObj = new Date(Date.parse(endDate));
const filteredData = itemdata.filter(item => {
const itemDate = new Date(Date.parse(item.date));
return itemDate >= startDateObj && itemDate <= endDateObj;
});
filteredData.forEach(element => {
console.log('wff');
console.log(element.value);
});
const dateRange = createDateRange(startDate, endDate);
const labels: string[] = dateRange.map(date => date.getDate() % 5 === 0 ? date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' }) : '');
setChartData({
labels,
datasets: [
{
label: type === 'humidity' ? 'Влажность' : 'Температура',
data: itemdata.map(item => item.value),
backgroundColor: [
'rgb(153, 102, 255)'
],
borderColor: [
'rgb(153, 102, 255)'
],
borderWidth: 1
}
]
} as any);
// setData({
// labels,
// datasets: [
// {
// label: 'Expenses by Month',
// data: filteredData.map(item => item.value),
// backgroundColor: [
// 'rgb(153, 102, 255)'
// ],
// borderColor: [
// 'rgb(153, 102, 255)'
// ],
// borderWidth: 1
// }
// ]
// } as any);
}
};
const options = {
scales: {
x: {
display: true,
title: {
display: true,
text: "Дата",
},
min: 0,
max: 100,
},
y: {
display: true,
title: {
display: true,
text: "Значение",
},
},
},
};
return (
<Form onFinish={handleGenerateReport} className="p-8" style={{ marginTop: '100px', marginLeft: '650px', height: '800px', width: '600px' }}>
<h1 className="text-2xl font-bold mb-4">Статистика по выбранной теплице</h1>
<div className="grid grid-cols-2 gap-4">
<DatePicker value={from} onChange={(date) => setFrom(date)} format="DD.MM.YYYY" />
<DatePicker value={to} onChange={(date) => setTo(date)} format="DD.MM.YYYY" />
</div>
<div className="m-4">
<Select value={type} onChange={(value) => setType(value)}>
<Select.Option value="humidity">Влажность</Select.Option>
<Select.Option value="temperature">Температура</Select.Option>
</Select>
</div>
<Button type="primary" htmlType="submit" disabled={isButtonDisabled}>
Сформировать отчёт
</Button>
<div className="m-4">
<Line data={chartData} options={options} />
</div>
</Form>
);
};

View File

@ -0,0 +1,20 @@
import { Table } from "antd";
import { HistoryData } from "../../core/services/greenhouse-service";
export interface GreenhouseCommandTableProps {
history: HistoryData[],
className: string
}
const GreenhouseCommandHistoryTable = (props: GreenhouseCommandTableProps) => {
const { history, className } = props;
return (
<Table columns={[
{ title: 'Действие', dataIndex: 'action' },
{ title: 'Время начала', dataIndex: 'startAt' },
{ title: 'Время конца', dataIndex: 'endAt'}
]} dataSource={history} className={className} />
);
}
export default GreenhouseCommandHistoryTable;

View File

@ -0,0 +1,46 @@
import { GreenhouseInfo } from "../../core/api/data-contracts";
export interface GreehouseDataDisplayProps {
info: GreenhouseInfo
}
const GreenhouseDataDisplay = (props: GreehouseDataDisplayProps) => {
const { info } = props;
return (
<div className="p-8">
<h1 className="text-4xl font-bold mb-4">Данные теплицы</h1>
<div className="bg-white shadow-md rounded-lg p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h2 className="text-xl font-semibold mb-2">Процент влажности почвы:</h2>
<p className="text-lg">{info.percentWater}%</p>
</div>
<div>
<h2 className="text-xl font-semibold mb-2">Температура почвы:</h2>
<p className="text-lg">{info.soilTemperature}°C</p>
</div>
<div>
<h2 className="text-xl font-semibold mb-2">Статус вентиля:</h2>
<p className="text-lg">
{info.pumpStatus ? 'Открыт' : 'Закрыт'}
</p>
</div>
<div>
<h2 className="text-xl font-semibold mb-2">Статус нагревателя:</h2>
<p className="text-lg">
{info.heatingStatus ? 'Включен' : 'Выключен'}
</p>
</div>
<div>
<h2 className="text-xl font-semibold mb-2">Статус автополива:</h2>
<p className="text-lg">
{info.autoWateringStatus ? 'Активирован' : 'Деактивирован'}
</p>
</div>
</div>
</div>
</div>
);
}
export default GreenhouseDataDisplay;

View File

@ -0,0 +1,26 @@
import { Switch } from "antd";
import { useState } from "react";
export interface GreenhouseManagerProps {
}
const GreenhouseManager = (props: GreenhouseManagerProps) => {
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Управление</h1>
<div className="m-4">
<label htmlFor="watering" className="text-xl me-2">Полив</label>
<Switch id="watering" checkedChildren="Вкл" unCheckedChildren="Выкл" />
</div>
<div className="m-4">
<label htmlFor="heating" className="text-xl me-2">Нагрев</label>
<Switch id="heating" checkedChildren="Вкл" unCheckedChildren="Выкл" />
</div>
</div>
);
};
export default GreenhouseManager;

View File

@ -0,0 +1,97 @@
import { useState } from "react";
import { Greenhouse, HeatingMode, WateringMode } from "../../core/api/data-contracts";
import { Select, Button, Form, Input } from "antd";
export interface GreenhouseSettingsFormProps {
updateSettings: (settings: Greenhouse) => void,
id: number
}
const GreenhouseSettingsForm = (props: GreenhouseSettingsFormProps) => {
const { updateSettings, id } = props;
const [settings, setSettings] = useState<Greenhouse>({
id: Number(id!),
wateringMode: WateringMode.Value0,
heatingMode: HeatingMode.Value0,
});
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Настройки</h1>
<Form
name="basic"
layout="vertical"
initialValues={{ remember: true }}
onFinish={(values: any) => updateSettings({ ...settings, ...values })}
onFinishFailed={(error: any) => console.log(error)}
>
<Form.Item
label="Режим полива"
name="wateringMode"
rules={[{ required: true, message: 'Выберите режим полива' }]}
>
<Select
value={settings.wateringMode}
onChange={(value) => setSettings({ ...settings, wateringMode: value })}
>
<Select.Option value={WateringMode.Value0}>Ручной</Select.Option>
<Select.Option value={WateringMode.Value1}>Автоматический</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Режим нагрева"
name="heatingMode"
rules={[{ required: true, message: 'Выберите режим нагрева' }]}
>
<Select
value={settings.heatingMode}
onChange={(value) => setSettings({ ...settings, heatingMode: value })}
>
<Select.Option value={HeatingMode.Value0}>Ручной</Select.Option>
<Select.Option value={HeatingMode.Value1}>Автоматический</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Минимальная температура"
name="minTemperature"
rules={[{ required: true, message: 'Введите минимальную температуру' }]}
>
<Input type="number" />
</Form.Item>
<Form.Item
label="Максимальная температура"
name="maxTemperature"
rules={[{ required: true, message: 'Введите максимальную температуру' }]}
>
<Input type="number" />
</Form.Item>
<Form.Item
label="Минимальная влажность почвы"
name="minHumidity"
rules={[{ required: true, message: 'Введите минимальную влажность почвы' }]}
>
<Input type="number" />
</Form.Item>
<Form.Item
label="Максимальная влажность почвы"
name="maxHumidity"
rules={[{ required: true, message: 'Введите максимальную влажность почвы' }]}
>
<Input type="number" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Обновить настройки
</Button>
</Form.Item>
</Form>
</div>
);
};
export default GreenhouseSettingsForm;

View File

@ -0,0 +1,297 @@
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
import {
Farm,
FarmRequest,
Greenhouse,
GreenhouseInfo,
GreenhouseRequest,
LoginRequest,
RegisterRequest,
ValveRequest,
} from "./data-contracts";
import { ContentType, HttpClient, RequestParams } from "./http-client";
export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
/**
* No description
*
* @tags Auth
* @name AuthRegisterCreate
* @request POST:/api/Auth/register
* @secure
* @response `200` `void` Success
*/
authRegisterCreate = (data: RegisterRequest, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/Auth/register`,
method: "POST",
body: data,
secure: true,
type: ContentType.Json,
...params,
});
/**
* No description
*
* @tags Auth
* @name AuthLoginCreate
* @request POST:/api/Auth/login
* @secure
* @response `200` `void` Success
*/
authLoginCreate = (data: LoginRequest, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/Auth/login`,
method: "POST",
body: data,
secure: true,
type: ContentType.Json,
...params,
});
/**
* No description
*
* @tags Auth
* @name AuthUserList
* @request GET:/api/Auth/user
* @secure
* @response `200` `void` Success
*/
authUserList = (params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/Auth/user`,
method: "GET",
secure: true,
...params,
});
/**
* No description
*
* @tags Farm
* @name UserFarmDetail
* @request GET:/api/user/{userId}/farm
* @secure
* @response `200` `(Farm)[]` Success
*/
userFarmDetail = (userId: number, params: RequestParams = {}) =>
this.request<Farm[], any>({
path: `/api/user/${userId}/farm`,
method: "GET",
secure: true,
format: "json",
...params,
});
/**
* No description
*
* @tags Farm
* @name UserFarmCreate
* @request POST:/api/user/{userId}/farm
* @secure
* @response `200` `Farm` Success
*/
userFarmCreate = (userId: number, data: FarmRequest, params: RequestParams = {}) =>
this.request<Farm, any>({
path: `/api/user/${userId}/farm`,
method: "POST",
body: data,
secure: true,
type: ContentType.Json,
format: "json",
...params,
});
/**
* No description
*
* @tags Farm
* @name UserFarmDetail2
* @request GET:/api/user/{userId}/farm/{farmId}
* @originalName userFarmDetail
* @duplicate
* @secure
* @response `200` `Farm` Success
*/
userFarmDetail2 = (userId: number, farmId: number, params: RequestParams = {}) =>
this.request<Farm, any>({
path: `/api/user/${userId}/farm/${farmId}`,
method: "GET",
secure: true,
format: "json",
...params,
});
/**
* No description
*
* @tags Farm
* @name UserFarmUpdate
* @request PUT:/api/user/{userId}/farm/{farmId}
* @secure
* @response `200` `Farm` Success
*/
userFarmUpdate = (userId: number, farmId: number, data: FarmRequest, params: RequestParams = {}) =>
this.request<Farm, any>({
path: `/api/user/${userId}/farm/${farmId}`,
method: "PUT",
body: data,
secure: true,
type: ContentType.Json,
format: "json",
...params,
});
/**
* No description
*
* @tags Farm
* @name UserFarmDelete
* @request DELETE:/api/user/{userId}/farm/{farmId}
* @secure
* @response `200` `void` Success
*/
userFarmDelete = (userId: number, farmId: number, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/user/${userId}/farm/${farmId}`,
method: "DELETE",
secure: true,
...params,
});
/**
* No description
*
* @tags Greenhouse
* @name FarmGreenhouseDetail
* @request GET:/api/farm/{farmId}/greenhouse
* @secure
* @response `200` `(GreenhouseInfo)[]` Success
*/
farmGreenhouseDetail = (farmId: number, params: RequestParams = {}) =>
this.request<GreenhouseInfo[], any>({
path: `/api/farm/${farmId}/greenhouse`,
method: "GET",
secure: true,
format: "json",
...params,
});
/**
* No description
*
* @tags Greenhouse
* @name FarmGreenhouseCreate
* @request POST:/api/farm/{farmId}/greenhouse
* @secure
* @response `200` `Greenhouse` Success
*/
farmGreenhouseCreate = (farmId: number, data: GreenhouseRequest, params: RequestParams = {}) =>
this.request<Greenhouse, any>({
path: `/api/farm/${farmId}/greenhouse`,
method: "POST",
body: data,
secure: true,
type: ContentType.Json,
format: "json",
...params,
});
/**
* No description
*
* @tags Greenhouse
* @name FarmGreenhouseDetail2
* @request GET:/api/farm/{farmId}/greenhouse/{greenhouseId}
* @originalName farmGreenhouseDetail
* @duplicate
* @secure
* @response `200` `GreenhouseInfo` Success
*/
farmGreenhouseDetail2 = (farmId: number, greenhouseId: number, params: RequestParams = {}) =>
this.request<GreenhouseInfo, any>({
path: `/api/farm/${farmId}/greenhouse/${greenhouseId}`,
method: "GET",
secure: true,
format: "json",
...params,
});
/**
* No description
*
* @tags Greenhouse
* @name FarmGreenhouseDelete
* @request DELETE:/api/farm/{farmId}/greenhouse/{greenhouseId}
* @secure
* @response `200` `void` Success
*/
farmGreenhouseDelete = (farmId: number, greenhouseId: number, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/farm/${farmId}/greenhouse/${greenhouseId}`,
method: "DELETE",
secure: true,
...params,
});
/**
* No description
*
* @tags Greenhouse
* @name FarmGreenhouseSettingsDetail
* @request GET:/api/farm/{farmId}/greenhouse/{greenhouseId}/settings
* @secure
* @response `200` `Greenhouse` Success
*/
farmGreenhouseSettingsDetail = (farmId: number, greenhouseId: number, params: RequestParams = {}) =>
this.request<Greenhouse, any>({
path: `/api/farm/${farmId}/greenhouse/${greenhouseId}/settings`,
method: "GET",
secure: true,
format: "json",
...params,
});
/**
* No description
*
* @tags Greenhouse
* @name FarmGreenhouseSettingsUpdate
* @request PUT:/api/farm/{farmId}/greenhouse/{greenhouseId}/settings
* @secure
* @response `200` `Greenhouse` Success
*/
farmGreenhouseSettingsUpdate = (
farmId: number,
greenhouseId: number,
data: GreenhouseRequest,
params: RequestParams = {},
) =>
this.request<Greenhouse, any>({
path: `/api/farm/${farmId}/greenhouse/${greenhouseId}/settings`,
method: "PUT",
body: data,
secure: true,
type: ContentType.Json,
format: "json",
...params,
});
/**
* No description
*
* @tags Valve
* @name FarmGreenhouseWateringCreate
* @request POST:/api/farm/{farmId}/greenhouse/{ghId}/watering
* @secure
* @response `200` `void` Success
*/
farmGreenhouseWateringCreate = (farmId: number, ghId: number, data: ValveRequest, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/farm/${farmId}/greenhouse/${ghId}/watering`,
method: "POST",
body: data,
secure: true,
type: ContentType.Json,
...params,
});
}

View File

@ -0,0 +1,92 @@
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export interface Farm {
/** @format int32 */
id?: number;
name?: string | null;
/** @format int32 */
userId?: number;
user?: User;
raspberryIP?: string | null;
}
export interface FarmRequest {
name?: string | null;
raspberryIP?: string | null;
}
export interface Greenhouse {
/** @format int32 */
id?: number;
/** @format int32 */
recomendedTemperature?: number;
wateringMode?: WateringMode;
heatingMode?: HeatingMode;
/** @format int32 */
farmId?: number;
farm?: Farm;
}
export interface GreenhouseInfo {
/** @format int32 */
id?: number;
/** @format int32 */
percentWater?: number;
/** @format int32 */
soilTemperature?: number;
pumpStatus?: boolean;
heatingStatus?: boolean;
autoWateringStatus?: boolean;
}
export interface GreenhouseRequest {
/** @format int32 */
recomendedTemperature?: number;
wateringMode?: WateringMode;
heatingMode?: HeatingMode;
}
/** @format int32 */
export enum HeatingMode {
Value0 = 0,
Value1 = 1,
}
export interface LoginRequest {
email?: string | null;
password?: string | null;
}
export interface RegisterRequest {
name?: string | null;
email?: string | null;
password?: string | null;
}
export interface User {
/** @format int32 */
id?: number;
name?: string | null;
email?: string | null;
password?: string | null;
farms?: Farm[] | null;
}
export interface ValveRequest {
action?: string | null;
}
/** @format int32 */
export enum WateringMode {
Value0 = 0,
Value1 = 1,
}

View File

@ -0,0 +1,220 @@
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
export interface FullRequestParams extends Omit<RequestInit, "body"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat;
/** request body */
body?: unknown;
/** base url */
baseUrl?: string;
/** request cancellation token */
cancelToken?: CancelToken;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
export interface ApiConfig<SecurityDataType = unknown> {
baseUrl?: string;
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
customFetch?: typeof fetch;
}
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
data: D;
error: E;
}
type CancelToken = Symbol | string | number;
export enum ContentType {
Json = "application/json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = "http://172.18.167.3:5124";
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private abortControllers = new Map<CancelToken, AbortController>();
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
private baseApiParams: RequestParams = {
credentials: "same-origin",
headers: {},
redirect: "follow",
referrerPolicy: "no-referrer",
};
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig);
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
};
protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key);
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
}
protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key]);
}
protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key];
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
}
protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {};
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
return keys
.map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key)))
.join("&");
}
protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery);
return queryString ? `?${queryString}` : "";
}
private contentFormatters: Record<ContentType, (input: any) => any> = {
[ContentType.Json]: (input: any) =>
input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
[ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input),
[ContentType.FormData]: (input: any) =>
Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
formData.append(
key,
property instanceof Blob
? property
: typeof property === "object" && property !== null
? JSON.stringify(property)
: `${property}`,
);
return formData;
}, new FormData()),
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
};
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
return abortController.signal;
}
return void 0;
}
const abortController = new AbortController();
this.abortControllers.set(cancelToken, abortController);
return abortController.signal;
};
public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
abortController.abort();
this.abortControllers.delete(cancelToken);
}
};
public request = async <T = any, E = any>({
body,
secure,
path,
type,
query,
format,
baseUrl,
cancelToken,
...params
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
const secureParams =
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
this.securityWorker &&
(await this.securityWorker(this.securityData))) ||
{};
const requestParams = this.mergeRequestParams(params, secureParams);
const queryString = query && this.toQueryString(query);
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
const responseFormat = format || requestParams.format;
return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, {
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
},
signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null,
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
}).then(async (response) => {
const r = response.clone() as HttpResponse<T, E>;
r.data = null as unknown as T;
r.error = null as unknown as E;
const data = !responseFormat
? r
: await response[responseFormat]()
.then((data) => {
if (r.ok) {
r.data = data;
} else {
r.error = data;
}
return r;
})
.catch((e) => {
r.error = e;
return r;
});
if (cancelToken) {
this.abortControllers.delete(cancelToken);
}
if (!response.ok) throw data;
return data;
});
};
}

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