From c8c736c2453a79b8bd1bf01916627f226f047a89 Mon Sep 17 00:00:00 2001 From: arsenez Date: Wed, 18 Jun 2025 22:13:26 +0300 Subject: [PATCH 001/152] Setup nix project for Auth API --- auth/.gitignore | 2 ++ auth/CMakeLists.txt | 8 ++++++++ auth/default.nix | 13 +++++++++++++ auth/shell.nix | 15 +++++++++++++++ auth/src/main.cpp | 7 +++++++ 5 files changed, 45 insertions(+) create mode 100644 auth/.gitignore create mode 100644 auth/CMakeLists.txt create mode 100644 auth/default.nix create mode 100644 auth/shell.nix create mode 100644 auth/src/main.cpp diff --git a/auth/.gitignore b/auth/.gitignore new file mode 100644 index 00000000..43cad3d1 --- /dev/null +++ b/auth/.gitignore @@ -0,0 +1,2 @@ +build +result diff --git a/auth/CMakeLists.txt b/auth/CMakeLists.txt new file mode 100644 index 00000000..39647a65 --- /dev/null +++ b/auth/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.30) +project(VCDAuth CXX) + +file(GLOB_RECURSE SRCS "src/*.cpp") + +find_package(Poco REQUIRED COMPONENTS Net) + +add_executable(${PROJECT_NAME} ${SRCS}) \ No newline at end of file diff --git a/auth/default.nix b/auth/default.nix new file mode 100644 index 00000000..dd2a14ee --- /dev/null +++ b/auth/default.nix @@ -0,0 +1,13 @@ +with import {}; + +stdenv.mkDerivation { + name = "VCDAuth"; + src = ./.; + + nativeBuildInputs = [ cmake ninja ]; + buildInputs = [ poco ]; + + configurePhase = "cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release"; + buildPhase = "cmake --build build --config Release"; + installPhase = "install -D build/VCDAuth $out/bin/VCDAuth"; +} \ No newline at end of file diff --git a/auth/shell.nix b/auth/shell.nix new file mode 100644 index 00000000..f07a760c --- /dev/null +++ b/auth/shell.nix @@ -0,0 +1,15 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = [ + pkgs.gcc + pkgs.cmake + pkgs.ninja + pkgs.poco + ]; + + shellHook = '' + echo "g++: $(g++ --version | head -n1)" + echo "cmake: $(cmake --version | head -n1)" + ''; +} \ No newline at end of file diff --git a/auth/src/main.cpp b/auth/src/main.cpp new file mode 100644 index 00000000..82e3e808 --- /dev/null +++ b/auth/src/main.cpp @@ -0,0 +1,7 @@ +#include +#include + +int main(int, char**) { + printf("Hello, world!\n"); + return EXIT_SUCCESS; +} \ No newline at end of file From d1ed7b2885cfbb2309c666a591586fd7a1c781e9 Mon Sep 17 00:00:00 2001 From: arsenez Date: Fri, 20 Jun 2025 01:58:00 +0300 Subject: [PATCH 002/152] Setup HTTP server and add Not Found handler --- auth/.gitignore | 1 + auth/CMakeLists.txt | 5 ++-- auth/src/api/AuthRequestHandlerFactory.cpp | 12 +++++++++ auth/src/api/AuthRequestHandlerFactory.hpp | 11 ++++++++ auth/src/api/handlers/NotFoundHandler.cpp | 13 +++++++++ auth/src/api/handlers/NotFoundHandler.hpp | 12 +++++++++ auth/src/app.cpp | 31 ++++++++++++++++++++++ auth/src/app.hpp | 12 +++++++++ auth/src/main.cpp | 10 +++---- 9 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 auth/src/api/AuthRequestHandlerFactory.cpp create mode 100644 auth/src/api/AuthRequestHandlerFactory.hpp create mode 100644 auth/src/api/handlers/NotFoundHandler.cpp create mode 100644 auth/src/api/handlers/NotFoundHandler.hpp create mode 100644 auth/src/app.cpp create mode 100644 auth/src/app.hpp diff --git a/auth/.gitignore b/auth/.gitignore index 43cad3d1..1ca7dd0e 100644 --- a/auth/.gitignore +++ b/auth/.gitignore @@ -1,2 +1,3 @@ +.cache build result diff --git a/auth/CMakeLists.txt b/auth/CMakeLists.txt index 39647a65..c704cce0 100644 --- a/auth/CMakeLists.txt +++ b/auth/CMakeLists.txt @@ -3,6 +3,7 @@ project(VCDAuth CXX) file(GLOB_RECURSE SRCS "src/*.cpp") -find_package(Poco REQUIRED COMPONENTS Net) +find_package(Poco REQUIRED COMPONENTS Net Util) -add_executable(${PROJECT_NAME} ${SRCS}) \ No newline at end of file +add_executable(${PROJECT_NAME} ${SRCS}) +target_link_libraries(${PROJECT_NAME} Poco::Util Poco::Net) diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp new file mode 100644 index 00000000..5846fa52 --- /dev/null +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -0,0 +1,12 @@ +#include "AuthRequestHandlerFactory.hpp" +#include +#include + +#include "handlers/NotFoundHandler.hpp" + +using Poco::Net::HTTPRequestHandler; +using Poco::Net::HTTPServerRequest; + +HTTPRequestHandler* AuthRequestHandlerFactory::createRequestHandler(const HTTPServerRequest& request) { + return new NotFoundHandler; +} diff --git a/auth/src/api/AuthRequestHandlerFactory.hpp b/auth/src/api/AuthRequestHandlerFactory.hpp new file mode 100644 index 00000000..f5386a60 --- /dev/null +++ b/auth/src/api/AuthRequestHandlerFactory.hpp @@ -0,0 +1,11 @@ +#pragma once +#include + +using Poco::Net::HTTPRequestHandlerFactory; +using Poco::Net::HTTPRequestHandler; +using Poco::Net::HTTPServerRequest; + +class AuthRequestHandlerFactory : public HTTPRequestHandlerFactory { +public: + HTTPRequestHandler * createRequestHandler(const HTTPServerRequest &request) override; +}; diff --git a/auth/src/api/handlers/NotFoundHandler.cpp b/auth/src/api/handlers/NotFoundHandler.cpp new file mode 100644 index 00000000..c602b479 --- /dev/null +++ b/auth/src/api/handlers/NotFoundHandler.cpp @@ -0,0 +1,13 @@ +#include "NotFoundHandler.hpp" +#include +#include +#include + +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; +using Poco::Net::HTTPResponse; + +void NotFoundHandler::handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) { + response.setStatusAndReason(HTTPResponse::HTTP_NOT_FOUND); + response.send(); +} diff --git a/auth/src/api/handlers/NotFoundHandler.hpp b/auth/src/api/handlers/NotFoundHandler.hpp new file mode 100644 index 00000000..9ec155d1 --- /dev/null +++ b/auth/src/api/handlers/NotFoundHandler.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +using Poco::Net::HTTPRequestHandler; +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; + +class NotFoundHandler : public HTTPRequestHandler { +public: + void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) override; +}; diff --git a/auth/src/app.cpp b/auth/src/app.cpp new file mode 100644 index 00000000..8f144d15 --- /dev/null +++ b/auth/src/app.cpp @@ -0,0 +1,31 @@ +#include "app.hpp" +#include "api/AuthRequestHandlerFactory.hpp" +#include +#include +#include +#include +#include +#include +#include + +using Poco::Net::ServerSocket; +using Poco::Net::HTTPServer; +using Poco::Net::HTTPServerParams; + +void AppAuthServer::initialize(Application& self) { + loadConfiguration(); + ServerApplication::initialize(self); +} + +int AppAuthServer::main(const std::vector& args) { + Poco::UInt16 port = config().getUInt16("HTTP.port"); + + ServerSocket socket(port); + HTTPServer httpServer(new AuthRequestHandlerFactory, socket, new HTTPServerParams); + + httpServer.start(); + waitForTerminationRequest(); + httpServer.stopAll(); + + return ExitCode::EXIT_OK; +} diff --git a/auth/src/app.hpp b/auth/src/app.hpp new file mode 100644 index 00000000..b9fd0bb8 --- /dev/null +++ b/auth/src/app.hpp @@ -0,0 +1,12 @@ +#pragma once +#include +#include +#include + +using Poco::Util::ServerApplication; + +class AppAuthServer : public ServerApplication { +protected: + void initialize(Application& self) override; + int main(const std::vector& args) override; +}; diff --git a/auth/src/main.cpp b/auth/src/main.cpp index 82e3e808..925f55eb 100644 --- a/auth/src/main.cpp +++ b/auth/src/main.cpp @@ -1,7 +1,5 @@ -#include -#include +#include "app.hpp" +#include +#include -int main(int, char**) { - printf("Hello, world!\n"); - return EXIT_SUCCESS; -} \ No newline at end of file +POCO_SERVER_MAIN(AppAuthServer); From b8e7fca9b6019427cd94bd2bcbd66e006801ddf6 Mon Sep 17 00:00:00 2001 From: arsenez Date: Fri, 27 Jun 2025 01:17:50 +0300 Subject: [PATCH 003/152] Connect to DB and implement password hasher --- auth/CMakeLists.txt | 5 +++-- auth/src/api/DBConnector.cpp | 3 +++ auth/src/api/DBConnector.hpp | 13 +++++++++++++ auth/src/api/PasswordHasher.cpp | 32 ++++++++++++++++++++++++++++++++ auth/src/api/PasswordHasher.hpp | 15 +++++++++++++++ auth/src/api/User.hpp | 32 ++++++++++++++++++++++++++++++++ auth/src/app.cpp | 11 +++++++++++ 7 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 auth/src/api/DBConnector.cpp create mode 100644 auth/src/api/DBConnector.hpp create mode 100644 auth/src/api/PasswordHasher.cpp create mode 100644 auth/src/api/PasswordHasher.hpp create mode 100644 auth/src/api/User.hpp diff --git a/auth/CMakeLists.txt b/auth/CMakeLists.txt index c704cce0..dd58a045 100644 --- a/auth/CMakeLists.txt +++ b/auth/CMakeLists.txt @@ -3,7 +3,8 @@ project(VCDAuth CXX) file(GLOB_RECURSE SRCS "src/*.cpp") -find_package(Poco REQUIRED COMPONENTS Net Util) +find_package(PostgreSQL REQUIRED) +find_package(Poco REQUIRED COMPONENTS Net Util Data DataPostgreSQL) add_executable(${PROJECT_NAME} ${SRCS}) -target_link_libraries(${PROJECT_NAME} Poco::Util Poco::Net) +target_link_libraries(${PROJECT_NAME} Poco::Util Poco::Net Poco::Data Poco::DataPostgreSQL) diff --git a/auth/src/api/DBConnector.cpp b/auth/src/api/DBConnector.cpp new file mode 100644 index 00000000..40a5ef02 --- /dev/null +++ b/auth/src/api/DBConnector.cpp @@ -0,0 +1,3 @@ +#include "DBConnector.hpp" + +DBConnector::DBConnector(Session& dbSession): m_db(dbSession) {} diff --git a/auth/src/api/DBConnector.hpp b/auth/src/api/DBConnector.hpp new file mode 100644 index 00000000..c8e7e633 --- /dev/null +++ b/auth/src/api/DBConnector.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +using Poco::Data::Session; + +class DBConnector { +public: + explicit DBConnector(Session& dbSession); + +private: + Session& m_db; +}; \ No newline at end of file diff --git a/auth/src/api/PasswordHasher.cpp b/auth/src/api/PasswordHasher.cpp new file mode 100644 index 00000000..d3581241 --- /dev/null +++ b/auth/src/api/PasswordHasher.cpp @@ -0,0 +1,32 @@ +#include "PasswordHasher.hpp" +#include +#include +#include +#include +#include +#include + +using Poco::PBKDF2Engine; +using Poco::HMACEngine; +using Poco::SHA1Engine; +using Poco::DigestEngine; + +std::array PasswordHasher::genSalt() { + std::array ret; + + for (size_t i = 0; i < 64; ++i) { + ret[i] = m_prng.nextChar(); + } + + return ret; +} + +std::string PasswordHasher::encryptPassword(const std::string& password, const std::array& salt) { + PBKDF2Engine> pbkdf2(std::string(salt.begin(), salt.end())); + pbkdf2.update(password); + return DigestEngine::digestToHex(pbkdf2.digest()); +} + +bool PasswordHasher::verifyPassword(const std::string& password, const std::string& hash, const std::array& salt) { + return hash == encryptPassword(password, salt); +} \ No newline at end of file diff --git a/auth/src/api/PasswordHasher.hpp b/auth/src/api/PasswordHasher.hpp new file mode 100644 index 00000000..3b01c130 --- /dev/null +++ b/auth/src/api/PasswordHasher.hpp @@ -0,0 +1,15 @@ +#pragma once +#include +#include +#include +#include + +class PasswordHasher { +public: + std::array genSalt(); + std::string encryptPassword(const std::string& password, const std::array& salt); + bool verifyPassword(const std::string& password, const std::string& hash, const std::array& salt); + +private: + Poco::Random m_prng; +}; \ No newline at end of file diff --git a/auth/src/api/User.hpp b/auth/src/api/User.hpp new file mode 100644 index 00000000..284d7e9b --- /dev/null +++ b/auth/src/api/User.hpp @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include + +class User { +public: + int getId() const; + const std::string& getUsername() const; + const std::string& getEmail() const; + const std::string& getName() const; + const std::string& getPasswordHash() const; + const std::string& getCreatedAt() const; + const std::array& getSalt() const; + + void setId(int id); + void setUsername(const std::string& username); + void setEmail(const std::string& email); + void setName(const std::string& name); + void setPasswordHash(const std::string& passwordHash); + void setCreatedAt(const std::string& createdAt); + void setSalt(const std::array& salt); + +private: + int m_id; + std::string m_username; + std::string m_email; + std::string m_name; + std::string m_passwordHash; + std::string m_createdAt; + std::array m_salt; +}; \ No newline at end of file diff --git a/auth/src/app.cpp b/auth/src/app.cpp index 8f144d15..ddcd463d 100644 --- a/auth/src/app.cpp +++ b/auth/src/app.cpp @@ -1,5 +1,7 @@ #include "app.hpp" #include "api/AuthRequestHandlerFactory.hpp" +#include "api/DBConnector.hpp" +#include "api/PasswordHasher.hpp" #include #include #include @@ -8,9 +10,13 @@ #include #include +#include +#include + using Poco::Net::ServerSocket; using Poco::Net::HTTPServer; using Poco::Net::HTTPServerParams; +using Poco::Data::Session; void AppAuthServer::initialize(Application& self) { loadConfiguration(); @@ -18,6 +24,11 @@ void AppAuthServer::initialize(Application& self) { } int AppAuthServer::main(const std::vector& args) { + Poco::Data::PostgreSQL::Connector::registerConnector(); + + Session dbSession("PostgreSQL", "host=localhost port=5432 user=VCD password=12345 dbname=VCD"); + DBConnector db(dbSession); + Poco::UInt16 port = config().getUInt16("HTTP.port"); ServerSocket socket(port); From c7486ef729ec2be8bbe49e7b8219e39a02fba4fd Mon Sep 17 00:00:00 2001 From: arsenez Date: Fri, 27 Jun 2025 17:18:31 +0300 Subject: [PATCH 004/152] Add type handler for User --- auth/src/api/DBConnector.hpp | 52 +++++++++++++++++++++++++++++++++++- auth/src/api/User.cpp | 13 +++++++++ auth/src/api/User.hpp | 8 ------ 3 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 auth/src/api/User.cpp diff --git a/auth/src/api/DBConnector.hpp b/auth/src/api/DBConnector.hpp index c8e7e633..b8544c4b 100644 --- a/auth/src/api/DBConnector.hpp +++ b/auth/src/api/DBConnector.hpp @@ -1,6 +1,13 @@ #pragma once +#include +#include +#include #include +#include +#include +#include +#include "User.hpp" using Poco::Data::Session; @@ -10,4 +17,47 @@ class DBConnector { private: Session& m_db; -}; \ No newline at end of file +}; + +namespace Poco::Data { + template<> + class TypeHandler { + static void bind(size_t pos, const User& obj, AbstractBinder::Ptr pBinder, AbstractBinder::Direction dir) { + TypeHandler::bind(pos++, obj.getId(), pBinder, dir); + TypeHandler::bind(pos++, obj.getUsername(), pBinder, dir); + TypeHandler::bind(pos++, obj.getEmail(), pBinder, dir); + TypeHandler::bind(pos++, obj.getName(), pBinder, dir); + TypeHandler::bind(pos++, obj.getCreatedAt(), pBinder, dir); + } + + static size_t size() { + return 5; + } + + static void prepare(size_t pos, const User& obj, AbstractPreparator::Ptr pPrepare) { + TypeHandler::prepare(pos++, obj.getId(), pPrepare); + TypeHandler::prepare(pos++, obj.getUsername(), pPrepare); + TypeHandler::prepare(pos++, obj.getEmail(), pPrepare); + TypeHandler::prepare(pos++, obj.getName(), pPrepare); + TypeHandler::prepare(pos++, obj.getCreatedAt(), pPrepare); + } + + static void extract(size_t pos, User& obj, const User& defVal, AbstractExtractor::Ptr pExt) { + int id; + std::string username; + std::string email; + std::string name; + std::string createdAt; + TypeHandler::extract(pos++, id, defVal.getId(), pExt); + TypeHandler::extract(pos++, username, defVal.getUsername(), pExt); + TypeHandler::extract(pos++, email, defVal.getEmail(), pExt); + TypeHandler::extract(pos++, name, defVal.getName(), pExt); + TypeHandler::extract(pos++, createdAt, defVal.getCreatedAt(), pExt); + obj.setId(id); + obj.setUsername(username); + obj.setEmail(email); + obj.setName(name); + obj.setCreatedAt(createdAt); + } + }; +} \ No newline at end of file diff --git a/auth/src/api/User.cpp b/auth/src/api/User.cpp new file mode 100644 index 00000000..6ab8d06a --- /dev/null +++ b/auth/src/api/User.cpp @@ -0,0 +1,13 @@ +#include "User.hpp" + +int User::getId() const { return m_id; } +const std::string& User::getUsername() const { return m_username; } +const std::string& User::getEmail() const { return m_email; } +const std::string& User::getName() const { return m_name; } +const std::string& User::getCreatedAt() const {return m_createdAt; } + +void User::setId(int id) { m_id = id; } +void User::setUsername(const std::string& username) { m_username = username; } +void User::setEmail(const std::string& email) { m_email = email; } +void User::setName(const std::string& name) { m_name = name; } +void User::setCreatedAt(const std::string& createdAt) { m_createdAt = createdAt; } diff --git a/auth/src/api/User.hpp b/auth/src/api/User.hpp index 284d7e9b..2a191583 100644 --- a/auth/src/api/User.hpp +++ b/auth/src/api/User.hpp @@ -1,6 +1,4 @@ #pragma once -#include -#include #include class User { @@ -9,24 +7,18 @@ class User { const std::string& getUsername() const; const std::string& getEmail() const; const std::string& getName() const; - const std::string& getPasswordHash() const; const std::string& getCreatedAt() const; - const std::array& getSalt() const; void setId(int id); void setUsername(const std::string& username); void setEmail(const std::string& email); void setName(const std::string& name); - void setPasswordHash(const std::string& passwordHash); void setCreatedAt(const std::string& createdAt); - void setSalt(const std::array& salt); private: int m_id; std::string m_username; std::string m_email; std::string m_name; - std::string m_passwordHash; std::string m_createdAt; - std::array m_salt; }; \ No newline at end of file From 3df7fbd8dc60da5313403c449cea1c68e6005e58 Mon Sep 17 00:00:00 2001 From: arsenez Date: Fri, 27 Jun 2025 20:19:31 +0300 Subject: [PATCH 005/152] Add auth to linting workflow --- .github/workflows/push.yml | 5 +- auth/.clang-format | 123 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 auth/.clang-format diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 774fdade..9184fe69 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -17,7 +17,10 @@ jobs: npx prettier UI --write git add UI git status - + - name: Run formatter for Auth + run: | + find auth/src -iname '*.cpp' -or -iname '*.hpp' | xargs clang-format -i + git add auth/src - name: Setup git user run: | diff --git a/auth/.clang-format b/auth/.clang-format new file mode 100644 index 00000000..5edb2153 --- /dev/null +++ b/auth/.clang-format @@ -0,0 +1,123 @@ +--- +BinPackArguments: false +BinPackParameters: false +BitFieldColonSpacing: Both +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakAfterAttributes: Never +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeConceptDeclarations: Always +BreakBeforeInlineASMColon: Always +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +BreakStringLiterals: true +ColumnLimit: 80 +CompactNamespaces: false +Cpp11BracedListStyle: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +FixNamespaceComments: true +IncludeBlocks: Regroup +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: false +IndentExternBlock: Indent +IndentGotoLabels: false +IndentPPDirectives: AfterHash +IndentRequiresClause: true +IndentWidth: 2 +IndentWrappedFunctionNames: false +InsertBraces: true +InsertNewlineAtEOF: true +# IntegerLiteralSeparator: +# Binary: 8 +# Decimal: 3 +# Hex: 4 +KeepEmptyLinesAtTheStartOfBlocks: false +LambdaBodyIndentation: OuterScope +LineEnding: LF +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +PPIndentWidth: -1 +PointerAlignment: Left +QualifierAlignment: Right +ReferenceAlignment: Pointer +ReflowComments: true +RequiresClausePosition: OwnLine +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Always +ShortNamespaceLines: 0 +SortIncludes: CaseSensitive +SortUsingDeclarations: LexicographicNumeric +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Always +SpacesInSquareBrackets: false +Standard: c++20 +TabWidth: 2 +AlignAfterOpenBracket: BlockIndent +AlignArrayOfStructures: Right +AlignConsecutiveAssignments: + Enabled: true + AcrossEmptyLines: false + AcrossComments: true + AlignCompound: true + PadOperators: true +AlignConsecutiveBitFields: + Enabled: true + AcrossEmptyLines: true + AcrossComments: true + AlignCompound: true + PadOperators: true +AlignConsecutiveDeclarations: + Enabled: true + AcrossEmptyLines: false + AcrossComments: true + AlignCompound: true + PadOperators: true +AlignConsecutiveMacros: + Enabled: true + AcrossEmptyLines: false + AcrossComments: true + AlignCompound: true + PadOperators: true +AlignEscapedNewlines: Right +AlignOperands: AlignAfterOperator +AllowAllArgumentsOnNextLine: true +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: true +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: true +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes From 5a3dc507d3c599a25a4e4b540c4ff6d8cf479309 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:21:37 +0000 Subject: [PATCH 006/152] Automated formatting --- auth/src/api/AuthRequestHandlerFactory.cpp | 11 ++- auth/src/api/AuthRequestHandlerFactory.hpp | 5 +- auth/src/api/DBConnector.cpp | 3 +- auth/src/api/DBConnector.hpp | 101 ++++++++++++--------- auth/src/api/PasswordHasher.cpp | 43 +++++---- auth/src/api/PasswordHasher.hpp | 20 ++-- auth/src/api/User.cpp | 26 ++++-- auth/src/api/User.hpp | 32 +++---- auth/src/api/handlers/NotFoundHandler.cpp | 13 ++- auth/src/api/handlers/NotFoundHandler.hpp | 3 +- auth/src/app.cpp | 46 ++++++---- auth/src/app.hpp | 4 +- auth/src/main.cpp | 1 + 13 files changed, 180 insertions(+), 128 deletions(-) diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp index 5846fa52..521c1c82 100644 --- a/auth/src/api/AuthRequestHandlerFactory.cpp +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -1,12 +1,15 @@ #include "AuthRequestHandlerFactory.hpp" -#include -#include #include "handlers/NotFoundHandler.hpp" +#include +#include + using Poco::Net::HTTPRequestHandler; using Poco::Net::HTTPServerRequest; -HTTPRequestHandler* AuthRequestHandlerFactory::createRequestHandler(const HTTPServerRequest& request) { - return new NotFoundHandler; +HTTPRequestHandler* +AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request +) { + return new NotFoundHandler; } diff --git a/auth/src/api/AuthRequestHandlerFactory.hpp b/auth/src/api/AuthRequestHandlerFactory.hpp index f5386a60..546d974c 100644 --- a/auth/src/api/AuthRequestHandlerFactory.hpp +++ b/auth/src/api/AuthRequestHandlerFactory.hpp @@ -1,11 +1,12 @@ #pragma once #include -using Poco::Net::HTTPRequestHandlerFactory; using Poco::Net::HTTPRequestHandler; +using Poco::Net::HTTPRequestHandlerFactory; using Poco::Net::HTTPServerRequest; class AuthRequestHandlerFactory : public HTTPRequestHandlerFactory { public: - HTTPRequestHandler * createRequestHandler(const HTTPServerRequest &request) override; + HTTPRequestHandler* createRequestHandler(HTTPServerRequest const& request + ) override; }; diff --git a/auth/src/api/DBConnector.cpp b/auth/src/api/DBConnector.cpp index 40a5ef02..5deec0dd 100644 --- a/auth/src/api/DBConnector.cpp +++ b/auth/src/api/DBConnector.cpp @@ -1,3 +1,4 @@ #include "DBConnector.hpp" -DBConnector::DBConnector(Session& dbSession): m_db(dbSession) {} +DBConnector::DBConnector(Session& dbSession) + : m_db(dbSession) {} diff --git a/auth/src/api/DBConnector.hpp b/auth/src/api/DBConnector.hpp index b8544c4b..2671bf02 100644 --- a/auth/src/api/DBConnector.hpp +++ b/auth/src/api/DBConnector.hpp @@ -1,5 +1,7 @@ #pragma once +#include "User.hpp" + #include #include #include @@ -7,57 +9,68 @@ #include #include #include -#include "User.hpp" using Poco::Data::Session; class DBConnector { public: - explicit DBConnector(Session& dbSession); + explicit DBConnector(Session& dbSession); private: - Session& m_db; + Session& m_db; }; namespace Poco::Data { - template<> - class TypeHandler { - static void bind(size_t pos, const User& obj, AbstractBinder::Ptr pBinder, AbstractBinder::Direction dir) { - TypeHandler::bind(pos++, obj.getId(), pBinder, dir); - TypeHandler::bind(pos++, obj.getUsername(), pBinder, dir); - TypeHandler::bind(pos++, obj.getEmail(), pBinder, dir); - TypeHandler::bind(pos++, obj.getName(), pBinder, dir); - TypeHandler::bind(pos++, obj.getCreatedAt(), pBinder, dir); - } - - static size_t size() { - return 5; - } - - static void prepare(size_t pos, const User& obj, AbstractPreparator::Ptr pPrepare) { - TypeHandler::prepare(pos++, obj.getId(), pPrepare); - TypeHandler::prepare(pos++, obj.getUsername(), pPrepare); - TypeHandler::prepare(pos++, obj.getEmail(), pPrepare); - TypeHandler::prepare(pos++, obj.getName(), pPrepare); - TypeHandler::prepare(pos++, obj.getCreatedAt(), pPrepare); - } - - static void extract(size_t pos, User& obj, const User& defVal, AbstractExtractor::Ptr pExt) { - int id; - std::string username; - std::string email; - std::string name; - std::string createdAt; - TypeHandler::extract(pos++, id, defVal.getId(), pExt); - TypeHandler::extract(pos++, username, defVal.getUsername(), pExt); - TypeHandler::extract(pos++, email, defVal.getEmail(), pExt); - TypeHandler::extract(pos++, name, defVal.getName(), pExt); - TypeHandler::extract(pos++, createdAt, defVal.getCreatedAt(), pExt); - obj.setId(id); - obj.setUsername(username); - obj.setEmail(email); - obj.setName(name); - obj.setCreatedAt(createdAt); - } - }; -} \ No newline at end of file + template<> + class TypeHandler< class User > { + static void bind( + size_t pos, + User const& obj, + AbstractBinder::Ptr pBinder, + AbstractBinder::Direction dir + ) { + TypeHandler< int >::bind(pos++, obj.getId(), pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.getUsername(), pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.getEmail(), pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.getName(), pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.getCreatedAt(), pBinder, dir); + } + + static size_t size() { return 5; } + + static void + prepare(size_t pos, User const& obj, AbstractPreparator::Ptr pPrepare) { + TypeHandler< int >::prepare(pos++, obj.getId(), pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.getUsername(), pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.getEmail(), pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.getName(), pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.getCreatedAt(), pPrepare); + } + + static void extract( + size_t pos, User& obj, User const& defVal, AbstractExtractor::Ptr pExt + ) { + int id; + std::string username; + std::string email; + std::string name; + std::string createdAt; + TypeHandler< int >::extract(pos++, id, defVal.getId(), pExt); + TypeHandler< std::string >::extract( + pos++, username, defVal.getUsername(), pExt + ); + TypeHandler< std::string >::extract( + pos++, email, defVal.getEmail(), pExt + ); + TypeHandler< std::string >::extract(pos++, name, defVal.getName(), pExt); + TypeHandler< std::string >::extract( + pos++, createdAt, defVal.getCreatedAt(), pExt + ); + obj.setId(id); + obj.setUsername(username); + obj.setEmail(email); + obj.setName(name); + obj.setCreatedAt(createdAt); + } + }; +} // namespace Poco::Data diff --git a/auth/src/api/PasswordHasher.cpp b/auth/src/api/PasswordHasher.cpp index d3581241..69dafe62 100644 --- a/auth/src/api/PasswordHasher.cpp +++ b/auth/src/api/PasswordHasher.cpp @@ -1,32 +1,39 @@ #include "PasswordHasher.hpp" + #include -#include -#include -#include #include +#include #include +#include +#include -using Poco::PBKDF2Engine; +using Poco::DigestEngine; using Poco::HMACEngine; +using Poco::PBKDF2Engine; using Poco::SHA1Engine; -using Poco::DigestEngine; -std::array PasswordHasher::genSalt() { - std::array ret; +std::array< uint8_t, 64 > PasswordHasher::genSalt() { + std::array< uint8_t, 64 > ret; - for (size_t i = 0; i < 64; ++i) { - ret[i] = m_prng.nextChar(); - } + for (size_t i = 0; i < 64; ++i) { ret[i] = m_prng.nextChar(); } - return ret; + return ret; } -std::string PasswordHasher::encryptPassword(const std::string& password, const std::array& salt) { - PBKDF2Engine> pbkdf2(std::string(salt.begin(), salt.end())); - pbkdf2.update(password); - return DigestEngine::digestToHex(pbkdf2.digest()); +std::string PasswordHasher::encryptPassword( + std::string const& password, std::array< uint8_t, 64 > const& salt +) { + PBKDF2Engine< HMACEngine< SHA1Engine > > pbkdf2( + std::string(salt.begin(), salt.end()) + ); + pbkdf2.update(password); + return DigestEngine::digestToHex(pbkdf2.digest()); } -bool PasswordHasher::verifyPassword(const std::string& password, const std::string& hash, const std::array& salt) { - return hash == encryptPassword(password, salt); -} \ No newline at end of file +bool PasswordHasher::verifyPassword( + std::string const& password, + std::string const& hash, + std::array< uint8_t, 64 > const& salt +) { + return hash == encryptPassword(password, salt); +} diff --git a/auth/src/api/PasswordHasher.hpp b/auth/src/api/PasswordHasher.hpp index 3b01c130..f2872157 100644 --- a/auth/src/api/PasswordHasher.hpp +++ b/auth/src/api/PasswordHasher.hpp @@ -1,15 +1,21 @@ #pragma once +#include +#include #include #include -#include -#include class PasswordHasher { public: - std::array genSalt(); - std::string encryptPassword(const std::string& password, const std::array& salt); - bool verifyPassword(const std::string& password, const std::string& hash, const std::array& salt); + std::array< uint8_t, 64 > genSalt(); + std::string encryptPassword( + std::string const& password, std::array< uint8_t, 64 > const& salt + ); + bool verifyPassword( + std::string const& password, + std::string const& hash, + std::array< uint8_t, 64 > const& salt + ); private: - Poco::Random m_prng; -}; \ No newline at end of file + Poco::Random m_prng; +}; diff --git a/auth/src/api/User.cpp b/auth/src/api/User.cpp index 6ab8d06a..973ef1d5 100644 --- a/auth/src/api/User.cpp +++ b/auth/src/api/User.cpp @@ -1,13 +1,23 @@ #include "User.hpp" int User::getId() const { return m_id; } -const std::string& User::getUsername() const { return m_username; } -const std::string& User::getEmail() const { return m_email; } -const std::string& User::getName() const { return m_name; } -const std::string& User::getCreatedAt() const {return m_createdAt; } + +std::string const& User::getUsername() const { return m_username; } + +std::string const& User::getEmail() const { return m_email; } + +std::string const& User::getName() const { return m_name; } + +std::string const& User::getCreatedAt() const { return m_createdAt; } void User::setId(int id) { m_id = id; } -void User::setUsername(const std::string& username) { m_username = username; } -void User::setEmail(const std::string& email) { m_email = email; } -void User::setName(const std::string& name) { m_name = name; } -void User::setCreatedAt(const std::string& createdAt) { m_createdAt = createdAt; } + +void User::setUsername(std::string const& username) { m_username = username; } + +void User::setEmail(std::string const& email) { m_email = email; } + +void User::setName(std::string const& name) { m_name = name; } + +void User::setCreatedAt(std::string const& createdAt) { + m_createdAt = createdAt; +} diff --git a/auth/src/api/User.hpp b/auth/src/api/User.hpp index 2a191583..d5b52019 100644 --- a/auth/src/api/User.hpp +++ b/auth/src/api/User.hpp @@ -3,22 +3,22 @@ class User { public: - int getId() const; - const std::string& getUsername() const; - const std::string& getEmail() const; - const std::string& getName() const; - const std::string& getCreatedAt() const; + int getId() const; + std::string const& getUsername() const; + std::string const& getEmail() const; + std::string const& getName() const; + std::string const& getCreatedAt() const; - void setId(int id); - void setUsername(const std::string& username); - void setEmail(const std::string& email); - void setName(const std::string& name); - void setCreatedAt(const std::string& createdAt); + void setId(int id); + void setUsername(std::string const& username); + void setEmail(std::string const& email); + void setName(std::string const& name); + void setCreatedAt(std::string const& createdAt); private: - int m_id; - std::string m_username; - std::string m_email; - std::string m_name; - std::string m_createdAt; -}; \ No newline at end of file + int m_id; + std::string m_username; + std::string m_email; + std::string m_name; + std::string m_createdAt; +}; diff --git a/auth/src/api/handlers/NotFoundHandler.cpp b/auth/src/api/handlers/NotFoundHandler.cpp index c602b479..e8a039d8 100644 --- a/auth/src/api/handlers/NotFoundHandler.cpp +++ b/auth/src/api/handlers/NotFoundHandler.cpp @@ -1,13 +1,16 @@ #include "NotFoundHandler.hpp" + #include -#include #include +#include +using Poco::Net::HTTPResponse; using Poco::Net::HTTPServerRequest; using Poco::Net::HTTPServerResponse; -using Poco::Net::HTTPResponse; -void NotFoundHandler::handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) { - response.setStatusAndReason(HTTPResponse::HTTP_NOT_FOUND); - response.send(); +void NotFoundHandler::handleRequest( + HTTPServerRequest& request, HTTPServerResponse& response +) { + response.setStatusAndReason(HTTPResponse::HTTP_NOT_FOUND); + response.send(); } diff --git a/auth/src/api/handlers/NotFoundHandler.hpp b/auth/src/api/handlers/NotFoundHandler.hpp index 9ec155d1..2fa9456c 100644 --- a/auth/src/api/handlers/NotFoundHandler.hpp +++ b/auth/src/api/handlers/NotFoundHandler.hpp @@ -8,5 +8,6 @@ using Poco::Net::HTTPServerResponse; class NotFoundHandler : public HTTPRequestHandler { public: - void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) override; + void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) + override; }; diff --git a/auth/src/app.cpp b/auth/src/app.cpp index ddcd463d..c9bdea54 100644 --- a/auth/src/app.cpp +++ b/auth/src/app.cpp @@ -1,42 +1,48 @@ #include "app.hpp" + #include "api/AuthRequestHandlerFactory.hpp" #include "api/DBConnector.hpp" #include "api/PasswordHasher.hpp" + +#include +#include +#include #include +#include #include #include -#include -#include #include #include -#include -#include - -using Poco::Net::ServerSocket; +using Poco::Data::Session; using Poco::Net::HTTPServer; using Poco::Net::HTTPServerParams; -using Poco::Data::Session; +using Poco::Net::ServerSocket; void AppAuthServer::initialize(Application& self) { - loadConfiguration(); - ServerApplication::initialize(self); + loadConfiguration(); + ServerApplication::initialize(self); } -int AppAuthServer::main(const std::vector& args) { - Poco::Data::PostgreSQL::Connector::registerConnector(); +int AppAuthServer::main(std::vector< std::string > const& args) { + Poco::Data::PostgreSQL::Connector::registerConnector(); - Session dbSession("PostgreSQL", "host=localhost port=5432 user=VCD password=12345 dbname=VCD"); - DBConnector db(dbSession); + Session dbSession( + "PostgreSQL", + "host=localhost port=5432 user=VCD password=12345 dbname=VCD" + ); + DBConnector db(dbSession); - Poco::UInt16 port = config().getUInt16("HTTP.port"); + Poco::UInt16 port = config().getUInt16("HTTP.port"); - ServerSocket socket(port); - HTTPServer httpServer(new AuthRequestHandlerFactory, socket, new HTTPServerParams); + ServerSocket socket(port); + HTTPServer httpServer( + new AuthRequestHandlerFactory, socket, new HTTPServerParams + ); - httpServer.start(); - waitForTerminationRequest(); - httpServer.stopAll(); + httpServer.start(); + waitForTerminationRequest(); + httpServer.stopAll(); - return ExitCode::EXIT_OK; + return ExitCode::EXIT_OK; } diff --git a/auth/src/app.hpp b/auth/src/app.hpp index b9fd0bb8..907d5701 100644 --- a/auth/src/app.hpp +++ b/auth/src/app.hpp @@ -7,6 +7,6 @@ using Poco::Util::ServerApplication; class AppAuthServer : public ServerApplication { protected: - void initialize(Application& self) override; - int main(const std::vector& args) override; + void initialize(Application& self) override; + int main(std::vector< std::string > const& args) override; }; diff --git a/auth/src/main.cpp b/auth/src/main.cpp index 925f55eb..300df80c 100644 --- a/auth/src/main.cpp +++ b/auth/src/main.cpp @@ -1,4 +1,5 @@ #include "app.hpp" + #include #include From a4c898ab77daf7a2f5270289eec82806445e16bb Mon Sep 17 00:00:00 2001 From: arsenez Date: Sat, 28 Jun 2025 00:10:05 +0300 Subject: [PATCH 007/152] Implement registration endpoint --- auth/src/api/AuthRequestHandlerFactory.cpp | 13 ++- auth/src/api/AuthRequestHandlerFactory.hpp | 11 ++- auth/src/api/DBConnector.cpp | 35 ++++++++ auth/src/api/DBConnector.hpp | 80 ++++++++++++++----- auth/src/api/PasswordHasher.cpp | 20 +++-- auth/src/api/PasswordHasher.hpp | 13 ++- auth/src/api/User.cpp | 23 ------ auth/src/api/User.hpp | 25 ++---- auth/src/api/UserCredentials.hpp | 9 +++ auth/src/api/handlers/NotFoundHandler.hpp | 5 +- auth/src/api/handlers/RegistrationHandler.cpp | 65 +++++++++++++++ auth/src/api/handlers/RegistrationHandler.hpp | 22 +++++ auth/src/app.cpp | 3 +- 13 files changed, 235 insertions(+), 89 deletions(-) delete mode 100644 auth/src/api/User.cpp create mode 100644 auth/src/api/UserCredentials.hpp create mode 100644 auth/src/api/handlers/RegistrationHandler.cpp create mode 100644 auth/src/api/handlers/RegistrationHandler.hpp diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp index 521c1c82..97863aff 100644 --- a/auth/src/api/AuthRequestHandlerFactory.cpp +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -1,6 +1,8 @@ #include "AuthRequestHandlerFactory.hpp" +#include "DBConnector.hpp" #include "handlers/NotFoundHandler.hpp" +#include "handlers/RegistrationHandler.hpp" #include #include @@ -8,8 +10,15 @@ using Poco::Net::HTTPRequestHandler; using Poco::Net::HTTPServerRequest; -HTTPRequestHandler* -AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request +AuthRequestHandlerFactory::AuthRequestHandlerFactory(DBConnector& db): m_db(db) {} + +HTTPRequestHandler* AuthRequestHandlerFactory::createRequestHandler( + HTTPServerRequest const& request ) { + if (request.getMethod() == "POST") { + if(request.getURI() == "/api/register") { + return new RegistrationHandler(m_db); + } + } return new NotFoundHandler; } diff --git a/auth/src/api/AuthRequestHandlerFactory.hpp b/auth/src/api/AuthRequestHandlerFactory.hpp index 546d974c..bf123017 100644 --- a/auth/src/api/AuthRequestHandlerFactory.hpp +++ b/auth/src/api/AuthRequestHandlerFactory.hpp @@ -1,4 +1,5 @@ #pragma once +#include "DBConnector.hpp" #include using Poco::Net::HTTPRequestHandler; @@ -7,6 +8,12 @@ using Poco::Net::HTTPServerRequest; class AuthRequestHandlerFactory : public HTTPRequestHandlerFactory { public: - HTTPRequestHandler* createRequestHandler(HTTPServerRequest const& request - ) override; + AuthRequestHandlerFactory(DBConnector& db); + +public: + HTTPRequestHandler* + createRequestHandler(HTTPServerRequest const& request) override; + +private: + DBConnector& m_db; }; diff --git a/auth/src/api/DBConnector.cpp b/auth/src/api/DBConnector.cpp index 5deec0dd..d705a6c1 100644 --- a/auth/src/api/DBConnector.cpp +++ b/auth/src/api/DBConnector.cpp @@ -1,4 +1,39 @@ #include "DBConnector.hpp" +#include "UserCredentials.hpp" +#include +#include +#include +#include +#include +#include + +using Poco::Data::Statement; +using namespace Poco::Data::Keywords; DBConnector::DBConnector(Session& dbSession) : m_db(dbSession) {} + +void DBConnector::createUser(UserCredentials user) { + Statement sql(m_db); + + try { + sql << "INSERT INTO users (name, username, email, salt, password_hash) VALUES ($1, $2, $3, $4, $5)", + use(user.name), + use(user.username), + use(user.email), + use(user.salt), + use(user.passwordHash); + + sql.execute(); + } catch (const Poco::Data::PostgreSQL::StatementException& e) { + // TODO: Handle username and email constraints + /* + if(strcmp(e.sqlState(), "23505") == 0) { + } + */ + throw; + } catch (const Poco::Exception& e) { + Poco::Util::Application::instance().logger().error("[ERROR] DBConnector::createUser: Exception %s:\n%s\n", std::string(e.className()), e.displayText()); + throw; + } +} \ No newline at end of file diff --git a/auth/src/api/DBConnector.hpp b/auth/src/api/DBConnector.hpp index 2671bf02..fbb056a0 100644 --- a/auth/src/api/DBConnector.hpp +++ b/auth/src/api/DBConnector.hpp @@ -1,6 +1,7 @@ #pragma once #include "User.hpp" +#include "UserCredentials.hpp" #include #include @@ -16,35 +17,38 @@ class DBConnector { public: explicit DBConnector(Session& dbSession); + void createUser(UserCredentials user); + private: Session& m_db; }; namespace Poco::Data { template<> - class TypeHandler< class User > { + class TypeHandler< struct User > { + public: static void bind( size_t pos, User const& obj, AbstractBinder::Ptr pBinder, AbstractBinder::Direction dir ) { - TypeHandler< int >::bind(pos++, obj.getId(), pBinder, dir); - TypeHandler< std::string >::bind(pos++, obj.getUsername(), pBinder, dir); - TypeHandler< std::string >::bind(pos++, obj.getEmail(), pBinder, dir); - TypeHandler< std::string >::bind(pos++, obj.getName(), pBinder, dir); - TypeHandler< std::string >::bind(pos++, obj.getCreatedAt(), pBinder, dir); + TypeHandler< int >::bind(pos++, obj.id, pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.username, pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.email, pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.name, pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.createdAt, pBinder, dir); } static size_t size() { return 5; } static void prepare(size_t pos, User const& obj, AbstractPreparator::Ptr pPrepare) { - TypeHandler< int >::prepare(pos++, obj.getId(), pPrepare); - TypeHandler< std::string >::prepare(pos++, obj.getUsername(), pPrepare); - TypeHandler< std::string >::prepare(pos++, obj.getEmail(), pPrepare); - TypeHandler< std::string >::prepare(pos++, obj.getName(), pPrepare); - TypeHandler< std::string >::prepare(pos++, obj.getCreatedAt(), pPrepare); + TypeHandler< int >::prepare(pos++, obj.id, pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.username, pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.email, pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.name, pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.createdAt, pPrepare); } static void extract( @@ -55,22 +59,56 @@ namespace Poco::Data { std::string email; std::string name; std::string createdAt; - TypeHandler< int >::extract(pos++, id, defVal.getId(), pExt); + TypeHandler< int >::extract(pos++, id, defVal.id, pExt); TypeHandler< std::string >::extract( - pos++, username, defVal.getUsername(), pExt + pos++, username, defVal.username, pExt ); TypeHandler< std::string >::extract( - pos++, email, defVal.getEmail(), pExt + pos++, email, defVal.email, pExt ); - TypeHandler< std::string >::extract(pos++, name, defVal.getName(), pExt); + TypeHandler< std::string >::extract(pos++, name, defVal.name, pExt); TypeHandler< std::string >::extract( - pos++, createdAt, defVal.getCreatedAt(), pExt + pos++, createdAt, defVal.createdAt, pExt ); - obj.setId(id); - obj.setUsername(username); - obj.setEmail(email); - obj.setName(name); - obj.setCreatedAt(createdAt); + obj.id = id; + obj.username = username; + obj.email = email; + obj.name = name; + obj.createdAt = createdAt; + } + }; + + template<> + class TypeHandler { + static void bind( + size_t pos, + UserCredentials const& obj, + AbstractBinder::Ptr pBinder, + AbstractBinder::Direction dir + ) { + TypeHandler::bind(pos, obj, pBinder, dir); + pos += TypeHandler::size(); + TypeHandler::bind(pos++, obj.passwordHash, pBinder, dir); + TypeHandler::bind(pos++, obj.salt, pBinder, dir); + } + + static size_t size() { return TypeHandler::size() + 2; } + + static void + prepare(size_t pos, UserCredentials const& obj, AbstractPreparator::Ptr pPrepare) { + TypeHandler::prepare(pos, obj, pPrepare); + pos += TypeHandler::size(); + TypeHandler::prepare(pos++, obj.passwordHash, pPrepare); + TypeHandler::prepare(pos++, obj.salt, pPrepare); + } + + static void extract( + size_t pos, UserCredentials& obj, UserCredentials const& defVal, AbstractExtractor::Ptr pExt + ) { + TypeHandler::extract(pos, obj, defVal, pExt); + pos += TypeHandler::size(); + TypeHandler::extract(pos++, obj.passwordHash, defVal.passwordHash, pExt); + TypeHandler::extract(pos++, obj.salt, defVal.salt, pExt); } }; } // namespace Poco::Data diff --git a/auth/src/api/PasswordHasher.cpp b/auth/src/api/PasswordHasher.cpp index 69dafe62..f93925f5 100644 --- a/auth/src/api/PasswordHasher.cpp +++ b/auth/src/api/PasswordHasher.cpp @@ -12,28 +12,26 @@ using Poco::HMACEngine; using Poco::PBKDF2Engine; using Poco::SHA1Engine; -std::array< uint8_t, 64 > PasswordHasher::genSalt() { - std::array< uint8_t, 64 > ret; +std::string PasswordHasher::genSalt() { + DigestEngine::Digest ret(32); - for (size_t i = 0; i < 64; ++i) { ret[i] = m_prng.nextChar(); } + for (size_t i = 0; i < ret.size(); ++i) { ret[i] = m_prng.nextChar(); } - return ret; + return DigestEngine::digestToHex(ret); } std::string PasswordHasher::encryptPassword( - std::string const& password, std::array< uint8_t, 64 > const& salt + std::string const& password, std::string const& salt ) { - PBKDF2Engine< HMACEngine< SHA1Engine > > pbkdf2( - std::string(salt.begin(), salt.end()) - ); + PBKDF2Engine< HMACEngine< SHA1Engine > > pbkdf2(salt); pbkdf2.update(password); return DigestEngine::digestToHex(pbkdf2.digest()); } bool PasswordHasher::verifyPassword( - std::string const& password, - std::string const& hash, - std::array< uint8_t, 64 > const& salt + std::string const& password, + std::string const& hash, + std::string const& salt ) { return hash == encryptPassword(password, salt); } diff --git a/auth/src/api/PasswordHasher.hpp b/auth/src/api/PasswordHasher.hpp index f2872157..2857d204 100644 --- a/auth/src/api/PasswordHasher.hpp +++ b/auth/src/api/PasswordHasher.hpp @@ -6,14 +6,13 @@ class PasswordHasher { public: - std::array< uint8_t, 64 > genSalt(); - std::string encryptPassword( - std::string const& password, std::array< uint8_t, 64 > const& salt - ); + std::string genSalt(); + std::string + encryptPassword(std::string const& password, std::string const& salt); bool verifyPassword( - std::string const& password, - std::string const& hash, - std::array< uint8_t, 64 > const& salt + std::string const& password, + std::string const& hash, + std::string const& salt ); private: diff --git a/auth/src/api/User.cpp b/auth/src/api/User.cpp deleted file mode 100644 index 973ef1d5..00000000 --- a/auth/src/api/User.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "User.hpp" - -int User::getId() const { return m_id; } - -std::string const& User::getUsername() const { return m_username; } - -std::string const& User::getEmail() const { return m_email; } - -std::string const& User::getName() const { return m_name; } - -std::string const& User::getCreatedAt() const { return m_createdAt; } - -void User::setId(int id) { m_id = id; } - -void User::setUsername(std::string const& username) { m_username = username; } - -void User::setEmail(std::string const& email) { m_email = email; } - -void User::setName(std::string const& name) { m_name = name; } - -void User::setCreatedAt(std::string const& createdAt) { - m_createdAt = createdAt; -} diff --git a/auth/src/api/User.hpp b/auth/src/api/User.hpp index d5b52019..07dc9168 100644 --- a/auth/src/api/User.hpp +++ b/auth/src/api/User.hpp @@ -1,24 +1,11 @@ #pragma once #include -class User { +struct User { public: - int getId() const; - std::string const& getUsername() const; - std::string const& getEmail() const; - std::string const& getName() const; - std::string const& getCreatedAt() const; - - void setId(int id); - void setUsername(std::string const& username); - void setEmail(std::string const& email); - void setName(std::string const& name); - void setCreatedAt(std::string const& createdAt); - -private: - int m_id; - std::string m_username; - std::string m_email; - std::string m_name; - std::string m_createdAt; + int id; + std::string username; + std::string email; + std::string name; + std::string createdAt; }; diff --git a/auth/src/api/UserCredentials.hpp b/auth/src/api/UserCredentials.hpp new file mode 100644 index 00000000..561f31de --- /dev/null +++ b/auth/src/api/UserCredentials.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "User.hpp" + +struct UserCredentials : public User { +public: + std::string passwordHash; + std::string salt; +}; \ No newline at end of file diff --git a/auth/src/api/handlers/NotFoundHandler.hpp b/auth/src/api/handlers/NotFoundHandler.hpp index 2fa9456c..a76a63ef 100644 --- a/auth/src/api/handlers/NotFoundHandler.hpp +++ b/auth/src/api/handlers/NotFoundHandler.hpp @@ -8,6 +8,7 @@ using Poco::Net::HTTPServerResponse; class NotFoundHandler : public HTTPRequestHandler { public: - void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) - override; + void handleRequest( + HTTPServerRequest& request, HTTPServerResponse& response + ) override; }; diff --git a/auth/src/api/handlers/RegistrationHandler.cpp b/auth/src/api/handlers/RegistrationHandler.cpp new file mode 100644 index 00000000..2bed3443 --- /dev/null +++ b/auth/src/api/handlers/RegistrationHandler.cpp @@ -0,0 +1,65 @@ +#include "RegistrationHandler.hpp" + +#include "../PasswordHasher.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Poco::Net::HTTPResponse; +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; +using Poco::Util::Application; +using Poco::Logger; +using Poco::JSON::Parser; +using Poco::Dynamic::Var; +using Poco::JSON::Object; + +RegistrationHandler::RegistrationHandler(DBConnector& db): m_db(db) {} + +void RegistrationHandler::handleRequest( + HTTPServerRequest& request, HTTPServerResponse& response +) { + Logger& logger = Application::instance().logger(); + logger.information("POST /api/register from %s", request.clientAddress().toString()); + + try { + if (!request.hasContentLength()) { + response.setStatusAndReason(HTTPResponse::HTTP_LENGTH_REQUIRED); + response.send(); + } else { + Parser parser; + PasswordHasher hasher; + UserCredentials newUser; + Var result = parser.parse(request.stream()); + Object::Ptr JSONObject = result.extract(); + + newUser.name = JSONObject->getValue("name"); + newUser.username = JSONObject->getValue("username"); + newUser.email = JSONObject->getValue("email"); + newUser.salt = hasher.genSalt(); + newUser.passwordHash = hasher.encryptPassword(JSONObject->getValue("password"), newUser.salt); + + m_db.createUser(newUser); + + response.setStatusAndReason(HTTPResponse::HTTP_CREATED); + response.send(); + } + } catch(const Poco::Data::PostgreSQL::StatementException& e) { + response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); + response.send(); + } catch (const Poco::Exception& e) { + response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); + response.send(); + logger.error("POST /api/register: Exception %s\n: %s\n", std::string(e.className()), e.displayText()); + } +} + diff --git a/auth/src/api/handlers/RegistrationHandler.hpp b/auth/src/api/handlers/RegistrationHandler.hpp new file mode 100644 index 00000000..acd24fbc --- /dev/null +++ b/auth/src/api/handlers/RegistrationHandler.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "../DBConnector.hpp" + +#include + +using Poco::Net::HTTPRequestHandler; +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; + +class RegistrationHandler: public HTTPRequestHandler { +public: + RegistrationHandler(DBConnector& db); + +public: + void handleRequest( + HTTPServerRequest& request, HTTPServerResponse& response + ) override; + +private: + DBConnector& m_db; +}; diff --git a/auth/src/app.cpp b/auth/src/app.cpp index c9bdea54..3db7b0ff 100644 --- a/auth/src/app.cpp +++ b/auth/src/app.cpp @@ -2,7 +2,6 @@ #include "api/AuthRequestHandlerFactory.hpp" #include "api/DBConnector.hpp" -#include "api/PasswordHasher.hpp" #include #include @@ -37,7 +36,7 @@ int AppAuthServer::main(std::vector< std::string > const& args) { ServerSocket socket(port); HTTPServer httpServer( - new AuthRequestHandlerFactory, socket, new HTTPServerParams + new AuthRequestHandlerFactory(db), socket, new HTTPServerParams ); httpServer.start(); From d1bb46168c947144ff584ec0ef3e6e9f252f15ed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:10:25 +0000 Subject: [PATCH 008/152] Automated formatting --- auth/src/api/AuthRequestHandlerFactory.cpp | 9 +- auth/src/api/AuthRequestHandlerFactory.hpp | 5 +- auth/src/api/DBConnector.cpp | 44 +++++----- auth/src/api/DBConnector.hpp | 52 ++++++------ auth/src/api/UserCredentials.hpp | 6 +- auth/src/api/handlers/NotFoundHandler.hpp | 5 +- auth/src/api/handlers/RegistrationHandler.cpp | 84 ++++++++++--------- auth/src/api/handlers/RegistrationHandler.hpp | 9 +- 8 files changed, 115 insertions(+), 99 deletions(-) diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp index 97863aff..ef7717a8 100644 --- a/auth/src/api/AuthRequestHandlerFactory.cpp +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -10,13 +10,14 @@ using Poco::Net::HTTPRequestHandler; using Poco::Net::HTTPServerRequest; -AuthRequestHandlerFactory::AuthRequestHandlerFactory(DBConnector& db): m_db(db) {} +AuthRequestHandlerFactory::AuthRequestHandlerFactory(DBConnector& db) + : m_db(db) {} -HTTPRequestHandler* AuthRequestHandlerFactory::createRequestHandler( - HTTPServerRequest const& request +HTTPRequestHandler* +AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request ) { if (request.getMethod() == "POST") { - if(request.getURI() == "/api/register") { + if (request.getURI() == "/api/register") { return new RegistrationHandler(m_db); } } diff --git a/auth/src/api/AuthRequestHandlerFactory.hpp b/auth/src/api/AuthRequestHandlerFactory.hpp index bf123017..e53668fd 100644 --- a/auth/src/api/AuthRequestHandlerFactory.hpp +++ b/auth/src/api/AuthRequestHandlerFactory.hpp @@ -1,5 +1,6 @@ #pragma once #include "DBConnector.hpp" + #include using Poco::Net::HTTPRequestHandler; @@ -11,8 +12,8 @@ class AuthRequestHandlerFactory : public HTTPRequestHandlerFactory { AuthRequestHandlerFactory(DBConnector& db); public: - HTTPRequestHandler* - createRequestHandler(HTTPServerRequest const& request) override; + HTTPRequestHandler* createRequestHandler(HTTPServerRequest const& request + ) override; private: DBConnector& m_db; diff --git a/auth/src/api/DBConnector.cpp b/auth/src/api/DBConnector.cpp index d705a6c1..c0ca412a 100644 --- a/auth/src/api/DBConnector.cpp +++ b/auth/src/api/DBConnector.cpp @@ -1,5 +1,7 @@ #include "DBConnector.hpp" + #include "UserCredentials.hpp" + #include #include #include @@ -14,26 +16,28 @@ DBConnector::DBConnector(Session& dbSession) : m_db(dbSession) {} void DBConnector::createUser(UserCredentials user) { - Statement sql(m_db); + Statement sql(m_db); - try { - sql << "INSERT INTO users (name, username, email, salt, password_hash) VALUES ($1, $2, $3, $4, $5)", - use(user.name), - use(user.username), - use(user.email), - use(user.salt), - use(user.passwordHash); + try { + sql << "INSERT INTO users (name, username, email, salt, password_hash) " + "VALUES ($1, $2, $3, $4, $5)", + use(user.name), use(user.username), use(user.email), use(user.salt), + use(user.passwordHash); - sql.execute(); - } catch (const Poco::Data::PostgreSQL::StatementException& e) { - // TODO: Handle username and email constraints - /* - if(strcmp(e.sqlState(), "23505") == 0) { - } - */ - throw; - } catch (const Poco::Exception& e) { - Poco::Util::Application::instance().logger().error("[ERROR] DBConnector::createUser: Exception %s:\n%s\n", std::string(e.className()), e.displayText()); - throw; + sql.execute(); + } catch (Poco::Data::PostgreSQL::StatementException const& e) { + // TODO: Handle username and email constraints + /* + if(strcmp(e.sqlState(), "23505") == 0) { } -} \ No newline at end of file + */ + throw; + } catch (Poco::Exception const& e) { + Poco::Util::Application::instance().logger().error( + "[ERROR] DBConnector::createUser: Exception %s:\n%s\n", + std::string(e.className()), + e.displayText() + ); + throw; + } +} diff --git a/auth/src/api/DBConnector.hpp b/auth/src/api/DBConnector.hpp index fbb056a0..740f9457 100644 --- a/auth/src/api/DBConnector.hpp +++ b/auth/src/api/DBConnector.hpp @@ -63,52 +63,56 @@ namespace Poco::Data { TypeHandler< std::string >::extract( pos++, username, defVal.username, pExt ); - TypeHandler< std::string >::extract( - pos++, email, defVal.email, pExt - ); + TypeHandler< std::string >::extract(pos++, email, defVal.email, pExt); TypeHandler< std::string >::extract(pos++, name, defVal.name, pExt); TypeHandler< std::string >::extract( pos++, createdAt, defVal.createdAt, pExt ); - obj.id = id; - obj.username = username; - obj.email = email; - obj.name = name; + obj.id = id; + obj.username = username; + obj.email = email; + obj.name = name; obj.createdAt = createdAt; } }; template<> - class TypeHandler { + class TypeHandler< struct UserCredentials > { static void bind( size_t pos, UserCredentials const& obj, AbstractBinder::Ptr pBinder, AbstractBinder::Direction dir ) { - TypeHandler::bind(pos, obj, pBinder, dir); - pos += TypeHandler::size(); - TypeHandler::bind(pos++, obj.passwordHash, pBinder, dir); - TypeHandler::bind(pos++, obj.salt, pBinder, dir); + TypeHandler< User >::bind(pos, obj, pBinder, dir); + pos += TypeHandler< User >::size(); + TypeHandler< std::string >::bind(pos++, obj.passwordHash, pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.salt, pBinder, dir); } - static size_t size() { return TypeHandler::size() + 2; } + static size_t size() { return TypeHandler< User >::size() + 2; } - static void - prepare(size_t pos, UserCredentials const& obj, AbstractPreparator::Ptr pPrepare) { - TypeHandler::prepare(pos, obj, pPrepare); - pos += TypeHandler::size(); - TypeHandler::prepare(pos++, obj.passwordHash, pPrepare); - TypeHandler::prepare(pos++, obj.salt, pPrepare); + static void prepare( + size_t pos, UserCredentials const& obj, AbstractPreparator::Ptr pPrepare + ) { + TypeHandler< User >::prepare(pos, obj, pPrepare); + pos += TypeHandler< User >::size(); + TypeHandler< std::string >::prepare(pos++, obj.passwordHash, pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.salt, pPrepare); } static void extract( - size_t pos, UserCredentials& obj, UserCredentials const& defVal, AbstractExtractor::Ptr pExt + size_t pos, + UserCredentials& obj, + UserCredentials const& defVal, + AbstractExtractor::Ptr pExt ) { - TypeHandler::extract(pos, obj, defVal, pExt); - pos += TypeHandler::size(); - TypeHandler::extract(pos++, obj.passwordHash, defVal.passwordHash, pExt); - TypeHandler::extract(pos++, obj.salt, defVal.salt, pExt); + TypeHandler< User >::extract(pos, obj, defVal, pExt); + pos += TypeHandler< User >::size(); + TypeHandler< std::string >::extract( + pos++, obj.passwordHash, defVal.passwordHash, pExt + ); + TypeHandler< std::string >::extract(pos++, obj.salt, defVal.salt, pExt); } }; } // namespace Poco::Data diff --git a/auth/src/api/UserCredentials.hpp b/auth/src/api/UserCredentials.hpp index 561f31de..a4445bc8 100644 --- a/auth/src/api/UserCredentials.hpp +++ b/auth/src/api/UserCredentials.hpp @@ -4,6 +4,6 @@ struct UserCredentials : public User { public: - std::string passwordHash; - std::string salt; -}; \ No newline at end of file + std::string passwordHash; + std::string salt; +}; diff --git a/auth/src/api/handlers/NotFoundHandler.hpp b/auth/src/api/handlers/NotFoundHandler.hpp index a76a63ef..2fa9456c 100644 --- a/auth/src/api/handlers/NotFoundHandler.hpp +++ b/auth/src/api/handlers/NotFoundHandler.hpp @@ -8,7 +8,6 @@ using Poco::Net::HTTPServerResponse; class NotFoundHandler : public HTTPRequestHandler { public: - void handleRequest( - HTTPServerRequest& request, HTTPServerResponse& response - ) override; + void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) + override; }; diff --git a/auth/src/api/handlers/RegistrationHandler.cpp b/auth/src/api/handlers/RegistrationHandler.cpp index 2bed3443..b3af81ad 100644 --- a/auth/src/api/handlers/RegistrationHandler.cpp +++ b/auth/src/api/handlers/RegistrationHandler.cpp @@ -2,64 +2,72 @@ #include "../PasswordHasher.hpp" +#include +#include #include +#include +#include #include #include #include #include #include -#include -#include -#include -#include #include +using Poco::Logger; +using Poco::Dynamic::Var; +using Poco::JSON::Object; +using Poco::JSON::Parser; using Poco::Net::HTTPResponse; using Poco::Net::HTTPServerRequest; using Poco::Net::HTTPServerResponse; using Poco::Util::Application; -using Poco::Logger; -using Poco::JSON::Parser; -using Poco::Dynamic::Var; -using Poco::JSON::Object; -RegistrationHandler::RegistrationHandler(DBConnector& db): m_db(db) {} +RegistrationHandler::RegistrationHandler(DBConnector& db) + : m_db(db) {} void RegistrationHandler::handleRequest( HTTPServerRequest& request, HTTPServerResponse& response ) { - Logger& logger = Application::instance().logger(); - logger.information("POST /api/register from %s", request.clientAddress().toString()); + Logger& logger = Application::instance().logger(); + logger.information( + "POST /api/register from %s", request.clientAddress().toString() + ); - try { - if (!request.hasContentLength()) { - response.setStatusAndReason(HTTPResponse::HTTP_LENGTH_REQUIRED); - response.send(); - } else { - Parser parser; - PasswordHasher hasher; - UserCredentials newUser; - Var result = parser.parse(request.stream()); - Object::Ptr JSONObject = result.extract(); + try { + if (!request.hasContentLength()) { + response.setStatusAndReason(HTTPResponse::HTTP_LENGTH_REQUIRED); + response.send(); + } else { + Parser parser; + PasswordHasher hasher; + UserCredentials newUser; + Var result = parser.parse(request.stream()); + Object::Ptr JSONObject = result.extract< Object::Ptr >(); - newUser.name = JSONObject->getValue("name"); - newUser.username = JSONObject->getValue("username"); - newUser.email = JSONObject->getValue("email"); - newUser.salt = hasher.genSalt(); - newUser.passwordHash = hasher.encryptPassword(JSONObject->getValue("password"), newUser.salt); + newUser.name = JSONObject->getValue< std::string >("name"); + newUser.username = JSONObject->getValue< std::string >("username"); + newUser.email = JSONObject->getValue< std::string >("email"); + newUser.salt = hasher.genSalt(); + newUser.passwordHash = hasher.encryptPassword( + JSONObject->getValue< std::string >("password"), newUser.salt + ); - m_db.createUser(newUser); + m_db.createUser(newUser); - response.setStatusAndReason(HTTPResponse::HTTP_CREATED); - response.send(); - } - } catch(const Poco::Data::PostgreSQL::StatementException& e) { - response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); - response.send(); - } catch (const Poco::Exception& e) { - response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); - response.send(); - logger.error("POST /api/register: Exception %s\n: %s\n", std::string(e.className()), e.displayText()); + response.setStatusAndReason(HTTPResponse::HTTP_CREATED); + response.send(); } + } catch (Poco::Data::PostgreSQL::StatementException const& e) { + response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); + response.send(); + } catch (Poco::Exception const& e) { + response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); + response.send(); + logger.error( + "POST /api/register: Exception %s\n: %s\n", + std::string(e.className()), + e.displayText() + ); + } } - diff --git a/auth/src/api/handlers/RegistrationHandler.hpp b/auth/src/api/handlers/RegistrationHandler.hpp index acd24fbc..dee97f7a 100644 --- a/auth/src/api/handlers/RegistrationHandler.hpp +++ b/auth/src/api/handlers/RegistrationHandler.hpp @@ -8,14 +8,13 @@ using Poco::Net::HTTPRequestHandler; using Poco::Net::HTTPServerRequest; using Poco::Net::HTTPServerResponse; -class RegistrationHandler: public HTTPRequestHandler { +class RegistrationHandler : public HTTPRequestHandler { public: - RegistrationHandler(DBConnector& db); + RegistrationHandler(DBConnector& db); public: - void handleRequest( - HTTPServerRequest& request, HTTPServerResponse& response - ) override; + void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) + override; private: DBConnector& m_db; From 1119ec1dec1b78864ba83b020da7fe1c481d1d0c Mon Sep 17 00:00:00 2001 From: arsenez Date: Sat, 28 Jun 2025 11:42:53 +0300 Subject: [PATCH 009/152] Handle unique username and email constrainsts --- auth/src/api/DBConnector.cpp | 13 ++++++++++--- auth/src/api/DBConnector.hpp | 3 +++ auth/src/api/handlers/RegistrationHandler.cpp | 10 ++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/auth/src/api/DBConnector.cpp b/auth/src/api/DBConnector.cpp index c0ca412a..89bf85fd 100644 --- a/auth/src/api/DBConnector.cpp +++ b/auth/src/api/DBConnector.cpp @@ -8,6 +8,7 @@ #include #include #include +#include using Poco::Data::Statement; using namespace Poco::Data::Keywords; @@ -26,11 +27,17 @@ void DBConnector::createUser(UserCredentials user) { sql.execute(); } catch (Poco::Data::PostgreSQL::StatementException const& e) { - // TODO: Handle username and email constraints - /* if(strcmp(e.sqlState(), "23505") == 0) { + std::regex regexp("Constraint: users_(username|email)_key"); + std::smatch match; + if (std::regex_search(e.message(), match, regexp)) { + if (match[1] == "username") { + throw UsernameExistsException(); + } else if (match[1] == "email") { + throw EmailExistsException(); + } + } } - */ throw; } catch (Poco::Exception const& e) { Poco::Util::Application::instance().logger().error( diff --git a/auth/src/api/DBConnector.hpp b/auth/src/api/DBConnector.hpp index 740f9457..d922c267 100644 --- a/auth/src/api/DBConnector.hpp +++ b/auth/src/api/DBConnector.hpp @@ -23,6 +23,9 @@ class DBConnector { Session& m_db; }; +class UsernameExistsException : public Poco::DataException {}; +class EmailExistsException : public Poco::DataException {}; + namespace Poco::Data { template<> class TypeHandler< struct User > { diff --git a/auth/src/api/handlers/RegistrationHandler.cpp b/auth/src/api/handlers/RegistrationHandler.cpp index b3af81ad..91ab07bb 100644 --- a/auth/src/api/handlers/RegistrationHandler.cpp +++ b/auth/src/api/handlers/RegistrationHandler.cpp @@ -58,6 +58,16 @@ void RegistrationHandler::handleRequest( response.setStatusAndReason(HTTPResponse::HTTP_CREATED); response.send(); } + } catch(const UsernameExistsException& e) { + std::string error = "username exists"; + response.setContentLength(error.length()); + response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); + response.send() << error; + } catch(const EmailExistsException& e) { + std::string error = "email exists"; + response.setContentLength(error.length()); + response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); + response.send() << error; } catch (Poco::Data::PostgreSQL::StatementException const& e) { response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); response.send(); From b64dd0681df7c580a30d2cee87c5513e8f3b8dd8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 08:43:26 +0000 Subject: [PATCH 010/152] Automated formatting --- auth/src/api/DBConnector.cpp | 6 +++--- auth/src/api/DBConnector.hpp | 1 + auth/src/api/handlers/RegistrationHandler.cpp | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/auth/src/api/DBConnector.cpp b/auth/src/api/DBConnector.cpp index 89bf85fd..38d7a975 100644 --- a/auth/src/api/DBConnector.cpp +++ b/auth/src/api/DBConnector.cpp @@ -7,8 +7,8 @@ #include #include #include -#include #include +#include using Poco::Data::Statement; using namespace Poco::Data::Keywords; @@ -27,8 +27,8 @@ void DBConnector::createUser(UserCredentials user) { sql.execute(); } catch (Poco::Data::PostgreSQL::StatementException const& e) { - if(strcmp(e.sqlState(), "23505") == 0) { - std::regex regexp("Constraint: users_(username|email)_key"); + if (strcmp(e.sqlState(), "23505") == 0) { + std::regex regexp("Constraint: users_(username|email)_key"); std::smatch match; if (std::regex_search(e.message(), match, regexp)) { if (match[1] == "username") { diff --git a/auth/src/api/DBConnector.hpp b/auth/src/api/DBConnector.hpp index d922c267..bad7f095 100644 --- a/auth/src/api/DBConnector.hpp +++ b/auth/src/api/DBConnector.hpp @@ -24,6 +24,7 @@ class DBConnector { }; class UsernameExistsException : public Poco::DataException {}; + class EmailExistsException : public Poco::DataException {}; namespace Poco::Data { diff --git a/auth/src/api/handlers/RegistrationHandler.cpp b/auth/src/api/handlers/RegistrationHandler.cpp index 91ab07bb..773c887a 100644 --- a/auth/src/api/handlers/RegistrationHandler.cpp +++ b/auth/src/api/handlers/RegistrationHandler.cpp @@ -58,12 +58,12 @@ void RegistrationHandler::handleRequest( response.setStatusAndReason(HTTPResponse::HTTP_CREATED); response.send(); } - } catch(const UsernameExistsException& e) { + } catch (UsernameExistsException const& e) { std::string error = "username exists"; response.setContentLength(error.length()); response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); response.send() << error; - } catch(const EmailExistsException& e) { + } catch (EmailExistsException const& e) { std::string error = "email exists"; response.setContentLength(error.length()); response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); From 4e94ac715cfd242303de8f6bc80f58f4a906df2a Mon Sep 17 00:00:00 2001 From: arsenez Date: Sat, 28 Jun 2025 12:04:35 +0300 Subject: [PATCH 011/152] Handle ContentType, JSON, and logic exceptions --- auth/src/api/handlers/RegistrationHandler.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/auth/src/api/handlers/RegistrationHandler.cpp b/auth/src/api/handlers/RegistrationHandler.cpp index 773c887a..b9560b08 100644 --- a/auth/src/api/handlers/RegistrationHandler.cpp +++ b/auth/src/api/handlers/RegistrationHandler.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,9 @@ void RegistrationHandler::handleRequest( if (!request.hasContentLength()) { response.setStatusAndReason(HTTPResponse::HTTP_LENGTH_REQUIRED); response.send(); + } else if (request.getContentType() != "application/json") { + response.setStatusAndReason(HTTPResponse::HTTP_UNSUPPORTED_MEDIA_TYPE); + response.send(); } else { Parser parser; PasswordHasher hasher; @@ -68,14 +72,19 @@ void RegistrationHandler::handleRequest( response.setContentLength(error.length()); response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); response.send() << error; - } catch (Poco::Data::PostgreSQL::StatementException const& e) { - response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); - response.send(); + } catch (Poco::LogicException const& e) { + response.setContentLength(e.message().length()); + response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); + response.send() << e.message(); + } catch (Poco::JSON::JSONException const& e) { + response.setContentLength(e.message().length()); + response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); + response.send() << e.message(); } catch (Poco::Exception const& e) { response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); response.send(); logger.error( - "POST /api/register: Exception %s\n: %s\n", + "[ERROR] POST /api/register: Exception %s:\n%s\n", std::string(e.className()), e.displayText() ); From 6c28a882d6a29428371d81fe86342bc60342b233 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 09:04:54 +0000 Subject: [PATCH 012/152] Automated formatting --- auth/src/api/handlers/RegistrationHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/src/api/handlers/RegistrationHandler.cpp b/auth/src/api/handlers/RegistrationHandler.cpp index b9560b08..4ac2bd8a 100644 --- a/auth/src/api/handlers/RegistrationHandler.cpp +++ b/auth/src/api/handlers/RegistrationHandler.cpp @@ -5,9 +5,9 @@ #include #include #include +#include #include #include -#include #include #include #include From 5886583994efbb6b2afa3db4cf6957004b80b36f Mon Sep 17 00:00:00 2001 From: arsenez Date: Sun, 29 Jun 2025 17:02:11 +0300 Subject: [PATCH 013/152] Implement POST /api/login --- auth/CMakeLists.txt | 4 +- auth/src/api/AuthRequestHandlerFactory.cpp | 8 +- auth/src/api/AuthRequestHandlerFactory.hpp | 4 +- auth/src/api/DBConnector.cpp | 124 ++++++++++++++++++ auth/src/api/DBConnector.hpp | 104 +-------------- auth/src/api/TokenManager.cpp | 44 +++++++ auth/src/api/TokenManager.hpp | 16 +++ auth/src/api/User.hpp | 10 +- auth/src/api/UserCredentials.hpp | 4 +- auth/src/api/handlers/LoginHandler.cpp | 83 ++++++++++++ auth/src/api/handlers/LoginHandler.hpp | 21 +++ auth/src/api/handlers/RegistrationHandler.cpp | 97 +++++++------- auth/src/app.cpp | 5 +- 13 files changed, 363 insertions(+), 161 deletions(-) create mode 100644 auth/src/api/TokenManager.cpp create mode 100644 auth/src/api/TokenManager.hpp create mode 100644 auth/src/api/handlers/LoginHandler.cpp create mode 100644 auth/src/api/handlers/LoginHandler.hpp diff --git a/auth/CMakeLists.txt b/auth/CMakeLists.txt index dd58a045..653cc40e 100644 --- a/auth/CMakeLists.txt +++ b/auth/CMakeLists.txt @@ -4,7 +4,7 @@ project(VCDAuth CXX) file(GLOB_RECURSE SRCS "src/*.cpp") find_package(PostgreSQL REQUIRED) -find_package(Poco REQUIRED COMPONENTS Net Util Data DataPostgreSQL) +find_package(Poco REQUIRED COMPONENTS Net JWT Util Data DataPostgreSQL) add_executable(${PROJECT_NAME} ${SRCS}) -target_link_libraries(${PROJECT_NAME} Poco::Util Poco::Net Poco::Data Poco::DataPostgreSQL) +target_link_libraries(${PROJECT_NAME} Poco::Util Poco::Net Poco::JWT Poco::Data Poco::DataPostgreSQL) diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp index ef7717a8..f777a50e 100644 --- a/auth/src/api/AuthRequestHandlerFactory.cpp +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -1,8 +1,10 @@ #include "AuthRequestHandlerFactory.hpp" #include "DBConnector.hpp" +#include "TokenManager.hpp" #include "handlers/NotFoundHandler.hpp" #include "handlers/RegistrationHandler.hpp" +#include "handlers/LoginHandler.hpp" #include #include @@ -10,8 +12,8 @@ using Poco::Net::HTTPRequestHandler; using Poco::Net::HTTPServerRequest; -AuthRequestHandlerFactory::AuthRequestHandlerFactory(DBConnector& db) - : m_db(db) {} +AuthRequestHandlerFactory::AuthRequestHandlerFactory(DBConnector& db, TokenManager& tokenManager) + : m_db(db), m_tokenManager(tokenManager) {} HTTPRequestHandler* AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request @@ -19,6 +21,8 @@ AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request if (request.getMethod() == "POST") { if (request.getURI() == "/api/register") { return new RegistrationHandler(m_db); + } else if (request.getURI() == "/api/login") { + return new LoginHandler(m_db, m_tokenManager); } } return new NotFoundHandler; diff --git a/auth/src/api/AuthRequestHandlerFactory.hpp b/auth/src/api/AuthRequestHandlerFactory.hpp index e53668fd..3744a6d9 100644 --- a/auth/src/api/AuthRequestHandlerFactory.hpp +++ b/auth/src/api/AuthRequestHandlerFactory.hpp @@ -1,5 +1,6 @@ #pragma once #include "DBConnector.hpp" +#include "TokenManager.hpp" #include @@ -9,7 +10,7 @@ using Poco::Net::HTTPServerRequest; class AuthRequestHandlerFactory : public HTTPRequestHandlerFactory { public: - AuthRequestHandlerFactory(DBConnector& db); + AuthRequestHandlerFactory(DBConnector& db, TokenManager& tokenManager); public: HTTPRequestHandler* createRequestHandler(HTTPServerRequest const& request @@ -17,4 +18,5 @@ class AuthRequestHandlerFactory : public HTTPRequestHandlerFactory { private: DBConnector& m_db; + TokenManager& m_tokenManager; }; diff --git a/auth/src/api/DBConnector.cpp b/auth/src/api/DBConnector.cpp index 38d7a975..89836bf0 100644 --- a/auth/src/api/DBConnector.cpp +++ b/auth/src/api/DBConnector.cpp @@ -9,6 +9,10 @@ #include #include #include +#include +#include +#include +#include using Poco::Data::Statement; using namespace Poco::Data::Keywords; @@ -48,3 +52,123 @@ void DBConnector::createUser(UserCredentials user) { throw; } } + +UserCredentials DBConnector::findUserWithCredentials(const std::string& login) { + Statement sql(m_db); + std::string clogin = login; + UserCredentials user; + + try { + sql << "SELECT id, username, email, name, created_at, password_hash, salt FROM users WHERE username = $1 OR email = $1", into(user), use(clogin); + + sql.execute(); + } catch (const Poco::Exception& e) { + Poco::Util::Application::instance().logger().error( + "[ERROR] DBConnector::findUser: Exception %s:\n%s", + std::string(e.className()), + e.displayText() + ); + throw; + } + + if (user.id == -1) { + throw UserNotFoundException(); + } + + return user; +} + +namespace Poco::Data { + template<> + class TypeHandler< struct User > { + public: + static void bind( + size_t pos, + User const& obj, + AbstractBinder::Ptr pBinder, + AbstractBinder::Direction dir + ) { + TypeHandler< int >::bind(pos++, obj.id, pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.username, pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.email, pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.name, pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.createdAt, pBinder, dir); + } + + static size_t size() { return 5; } + + static void + prepare(size_t pos, User const& obj, AbstractPreparator::Ptr pPrepare) { + TypeHandler< int >::prepare(pos++, obj.id, pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.username, pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.email, pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.name, pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.createdAt, pPrepare); + } + + static void extract( + size_t pos, User& obj, User const& defVal, AbstractExtractor::Ptr pExt + ) { + int id; + std::string username; + std::string email; + std::string name; + std::string createdAt; + TypeHandler< int >::extract(pos++, id, defVal.id, pExt); + TypeHandler< std::string >::extract( + pos++, username, defVal.username, pExt + ); + TypeHandler< std::string >::extract(pos++, email, defVal.email, pExt); + TypeHandler< std::string >::extract(pos++, name, defVal.name, pExt); + TypeHandler< std::string >::extract( + pos++, createdAt, defVal.createdAt, pExt + ); + obj.id = id; + obj.username = username; + obj.email = email; + obj.name = name; + obj.createdAt = createdAt; + } + }; + + template<> + class TypeHandler< struct UserCredentials > { + public: + static void bind( + size_t pos, + UserCredentials const& obj, + AbstractBinder::Ptr pBinder, + AbstractBinder::Direction dir + ) { + TypeHandler< User >::bind(pos, obj, pBinder, dir); + pos += TypeHandler< User >::size(); + TypeHandler< std::string >::bind(pos++, obj.passwordHash, pBinder, dir); + TypeHandler< std::string >::bind(pos++, obj.salt, pBinder, dir); + } + + static size_t size() { return TypeHandler< User >::size() + 2; } + + static void prepare( + size_t pos, UserCredentials const& obj, AbstractPreparator::Ptr pPrepare + ) { + TypeHandler< User >::prepare(pos, obj, pPrepare); + pos += TypeHandler< User >::size(); + TypeHandler< std::string >::prepare(pos++, obj.passwordHash, pPrepare); + TypeHandler< std::string >::prepare(pos++, obj.salt, pPrepare); + } + + static void extract( + size_t pos, + UserCredentials& obj, + UserCredentials const& defVal, + AbstractExtractor::Ptr pExt + ) { + TypeHandler< User >::extract(pos, obj, defVal, pExt); + pos += TypeHandler< User >::size(); + TypeHandler< std::string >::extract( + pos++, obj.passwordHash, defVal.passwordHash, pExt + ); + TypeHandler< std::string >::extract(pos++, obj.salt, defVal.salt, pExt); + } + }; +} // namespace Poco::Data \ No newline at end of file diff --git a/auth/src/api/DBConnector.hpp b/auth/src/api/DBConnector.hpp index bad7f095..9a5b3579 100644 --- a/auth/src/api/DBConnector.hpp +++ b/auth/src/api/DBConnector.hpp @@ -1,15 +1,9 @@ #pragma once -#include "User.hpp" #include "UserCredentials.hpp" -#include -#include -#include #include -#include -#include -#include +#include using Poco::Data::Session; @@ -19,6 +13,8 @@ class DBConnector { void createUser(UserCredentials user); + UserCredentials findUserWithCredentials(const std::string& login); + private: Session& m_db; }; @@ -27,96 +23,4 @@ class UsernameExistsException : public Poco::DataException {}; class EmailExistsException : public Poco::DataException {}; -namespace Poco::Data { - template<> - class TypeHandler< struct User > { - public: - static void bind( - size_t pos, - User const& obj, - AbstractBinder::Ptr pBinder, - AbstractBinder::Direction dir - ) { - TypeHandler< int >::bind(pos++, obj.id, pBinder, dir); - TypeHandler< std::string >::bind(pos++, obj.username, pBinder, dir); - TypeHandler< std::string >::bind(pos++, obj.email, pBinder, dir); - TypeHandler< std::string >::bind(pos++, obj.name, pBinder, dir); - TypeHandler< std::string >::bind(pos++, obj.createdAt, pBinder, dir); - } - - static size_t size() { return 5; } - - static void - prepare(size_t pos, User const& obj, AbstractPreparator::Ptr pPrepare) { - TypeHandler< int >::prepare(pos++, obj.id, pPrepare); - TypeHandler< std::string >::prepare(pos++, obj.username, pPrepare); - TypeHandler< std::string >::prepare(pos++, obj.email, pPrepare); - TypeHandler< std::string >::prepare(pos++, obj.name, pPrepare); - TypeHandler< std::string >::prepare(pos++, obj.createdAt, pPrepare); - } - - static void extract( - size_t pos, User& obj, User const& defVal, AbstractExtractor::Ptr pExt - ) { - int id; - std::string username; - std::string email; - std::string name; - std::string createdAt; - TypeHandler< int >::extract(pos++, id, defVal.id, pExt); - TypeHandler< std::string >::extract( - pos++, username, defVal.username, pExt - ); - TypeHandler< std::string >::extract(pos++, email, defVal.email, pExt); - TypeHandler< std::string >::extract(pos++, name, defVal.name, pExt); - TypeHandler< std::string >::extract( - pos++, createdAt, defVal.createdAt, pExt - ); - obj.id = id; - obj.username = username; - obj.email = email; - obj.name = name; - obj.createdAt = createdAt; - } - }; - - template<> - class TypeHandler< struct UserCredentials > { - static void bind( - size_t pos, - UserCredentials const& obj, - AbstractBinder::Ptr pBinder, - AbstractBinder::Direction dir - ) { - TypeHandler< User >::bind(pos, obj, pBinder, dir); - pos += TypeHandler< User >::size(); - TypeHandler< std::string >::bind(pos++, obj.passwordHash, pBinder, dir); - TypeHandler< std::string >::bind(pos++, obj.salt, pBinder, dir); - } - - static size_t size() { return TypeHandler< User >::size() + 2; } - - static void prepare( - size_t pos, UserCredentials const& obj, AbstractPreparator::Ptr pPrepare - ) { - TypeHandler< User >::prepare(pos, obj, pPrepare); - pos += TypeHandler< User >::size(); - TypeHandler< std::string >::prepare(pos++, obj.passwordHash, pPrepare); - TypeHandler< std::string >::prepare(pos++, obj.salt, pPrepare); - } - - static void extract( - size_t pos, - UserCredentials& obj, - UserCredentials const& defVal, - AbstractExtractor::Ptr pExt - ) { - TypeHandler< User >::extract(pos, obj, defVal, pExt); - pos += TypeHandler< User >::size(); - TypeHandler< std::string >::extract( - pos++, obj.passwordHash, defVal.passwordHash, pExt - ); - TypeHandler< std::string >::extract(pos++, obj.salt, defVal.salt, pExt); - } - }; -} // namespace Poco::Data +class UserNotFoundException : public Poco::DataException {}; diff --git a/auth/src/api/TokenManager.cpp b/auth/src/api/TokenManager.cpp new file mode 100644 index 00000000..a6d4a0c3 --- /dev/null +++ b/auth/src/api/TokenManager.cpp @@ -0,0 +1,44 @@ +#include "TokenManager.hpp" +#include "PasswordHasher.hpp" +#include "User.hpp" +#include +#include +#include +#include +#include + +using Poco::JWT::Token; +using Poco::JSON::Object; +using Poco::JSON::Stringifier; + +TokenManager::TokenManager() { + PasswordHasher hasher; + m_signer.setHMACKey(hasher.genSalt()); +} + +std::string TokenManager::generate(const User& user) { + Token refresh, access; + Object payload; + Object ret; + std::stringstream tmp; + + payload.set("id", user.id); + payload.set("username", user.username); + payload.set("email", user.email); + payload.set("name", user.name); + payload.set("createdAt", user.createdAt); + + refresh.payload() = payload; + refresh.setSubject("VCD JWT Refresh"); + refresh.setIssuedAt(Poco::Timestamp()); + + access.payload() = payload; + access.setSubject("VCD JWT Access"); + access.setIssuedAt(Poco::Timestamp()); + + ret.set("refresh", m_signer.sign(refresh, Signer::ALGO_HS256)); + ret.set("access", m_signer.sign(access, Signer::ALGO_HS256)); + + Stringifier::stringify(ret, tmp); + return tmp.rdbuf()->str(); +} \ No newline at end of file diff --git a/auth/src/api/TokenManager.hpp b/auth/src/api/TokenManager.hpp new file mode 100644 index 00000000..4465f14e --- /dev/null +++ b/auth/src/api/TokenManager.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include "User.hpp" + +using Poco::JWT::Signer; + +class TokenManager { +public: + TokenManager(); +public: + std::string generate(const User& user); +private: + Signer m_signer; +}; \ No newline at end of file diff --git a/auth/src/api/User.hpp b/auth/src/api/User.hpp index 07dc9168..55a0afd4 100644 --- a/auth/src/api/User.hpp +++ b/auth/src/api/User.hpp @@ -3,9 +3,9 @@ struct User { public: - int id; - std::string username; - std::string email; - std::string name; - std::string createdAt; + int id = -1; + std::string username = ""; + std::string email = ""; + std::string name = ""; + std::string createdAt = ""; }; diff --git a/auth/src/api/UserCredentials.hpp b/auth/src/api/UserCredentials.hpp index a4445bc8..34e4d56c 100644 --- a/auth/src/api/UserCredentials.hpp +++ b/auth/src/api/UserCredentials.hpp @@ -4,6 +4,6 @@ struct UserCredentials : public User { public: - std::string passwordHash; - std::string salt; + std::string passwordHash = ""; + std::string salt = ""; }; diff --git a/auth/src/api/handlers/LoginHandler.cpp b/auth/src/api/handlers/LoginHandler.cpp new file mode 100644 index 00000000..52364b46 --- /dev/null +++ b/auth/src/api/handlers/LoginHandler.cpp @@ -0,0 +1,83 @@ +#include "LoginHandler.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../PasswordHasher.hpp" +#include "../TokenManager.hpp" +#include + +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; +using Poco::Net::HTTPResponse; +using Poco::JSON::Parser; +using Poco::Dynamic::Var; +using Poco::JSON::Object; +using Poco::Logger; + +LoginHandler::LoginHandler(DBConnector& db, TokenManager& tokenManager): m_db(db), m_tokenManager(tokenManager) {} + +void LoginHandler::handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) { + Logger& logger = Poco::Util::Application::instance().logger(); + logger.information("POST /api/login from %s", request.clientAddress().toString()); + + if (request.getContentType() != "application/json") { + response.set("Accept-Post", "application/json; charset=UTF-8"); + response.setStatusAndReason(HTTPResponse::HTTP_UNSUPPORTED_MEDIA_TYPE); + response.send(); + } else if (!request.hasContentLength()) { + response.setStatusAndReason(HTTPResponse::HTTP_LENGTH_REQUIRED); + response.send(); + } else { + try { + std::string login; + std::string password; + UserCredentials user; + + PasswordHasher hasher; + + Parser parser; + Var result = parser.parse(request.stream()); + Object::Ptr JSONObject = result.extract(); + login = JSONObject->getValue("login"); + password = JSONObject->getValue("password"); + + user = m_db.findUserWithCredentials(login); + + if (hasher.verifyPassword(password, user.passwordHash, user.salt)) { + std::string tokens = m_tokenManager.generate(user); + response.setContentType("application/json"); + response.setContentLength(tokens.length()); + response.setStatusAndReason(HTTPResponse::HTTP_OK); + response.send() << tokens; + } else { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } + } catch (const UserNotFoundException& e) { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } catch (Poco::LogicException const& e) { + response.setContentLength(e.message().length()); + response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); + response.send() << e.message(); + } catch (Poco::JSON::JSONException const& e) { + response.setContentLength(e.message().length()); + response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); + response.send() << e.message(); + } catch (const Poco::Exception& e) { + response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); + response.send(); + logger.error("[ERROR] POST /api/login: Exception %s:\n%s", std::string(e.className()), e.displayText()); + } + } +} \ No newline at end of file diff --git a/auth/src/api/handlers/LoginHandler.hpp b/auth/src/api/handlers/LoginHandler.hpp new file mode 100644 index 00000000..a0bb5924 --- /dev/null +++ b/auth/src/api/handlers/LoginHandler.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "../DBConnector.hpp" +#include "../TokenManager.hpp" + +using Poco::Net::HTTPRequestHandler; +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; + +class LoginHandler : public HTTPRequestHandler { +public: + LoginHandler(DBConnector& m_db, TokenManager& tokenManager); + +public: + void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) override; + +private: + DBConnector& m_db; + TokenManager& m_tokenManager; +}; \ No newline at end of file diff --git a/auth/src/api/handlers/RegistrationHandler.cpp b/auth/src/api/handlers/RegistrationHandler.cpp index 4ac2bd8a..2fbec018 100644 --- a/auth/src/api/handlers/RegistrationHandler.cpp +++ b/auth/src/api/handlers/RegistrationHandler.cpp @@ -35,58 +35,59 @@ void RegistrationHandler::handleRequest( "POST /api/register from %s", request.clientAddress().toString() ); - try { - if (!request.hasContentLength()) { - response.setStatusAndReason(HTTPResponse::HTTP_LENGTH_REQUIRED); - response.send(); - } else if (request.getContentType() != "application/json") { - response.setStatusAndReason(HTTPResponse::HTTP_UNSUPPORTED_MEDIA_TYPE); - response.send(); - } else { - Parser parser; - PasswordHasher hasher; - UserCredentials newUser; - Var result = parser.parse(request.stream()); - Object::Ptr JSONObject = result.extract< Object::Ptr >(); + if (!request.hasContentLength()) { + response.setStatusAndReason(HTTPResponse::HTTP_LENGTH_REQUIRED); + response.send(); + } else if (request.getContentType() != "application/json") { + response.set("Accept-Post", "application/json; charset=UTF-8"); + response.setStatusAndReason(HTTPResponse::HTTP_UNSUPPORTED_MEDIA_TYPE); + response.send(); + } else { + try { + Parser parser; + PasswordHasher hasher; + UserCredentials newUser; + Var result = parser.parse(request.stream()); + Object::Ptr JSONObject = result.extract< Object::Ptr >(); - newUser.name = JSONObject->getValue< std::string >("name"); - newUser.username = JSONObject->getValue< std::string >("username"); - newUser.email = JSONObject->getValue< std::string >("email"); - newUser.salt = hasher.genSalt(); - newUser.passwordHash = hasher.encryptPassword( - JSONObject->getValue< std::string >("password"), newUser.salt - ); + newUser.name = JSONObject->getValue< std::string >("name"); + newUser.username = JSONObject->getValue< std::string >("username"); + newUser.email = JSONObject->getValue< std::string >("email"); + newUser.salt = hasher.genSalt(); + newUser.passwordHash = hasher.encryptPassword( + JSONObject->getValue< std::string >("password"), newUser.salt + ); - m_db.createUser(newUser); + m_db.createUser(newUser); - response.setStatusAndReason(HTTPResponse::HTTP_CREATED); + response.setStatusAndReason(HTTPResponse::HTTP_CREATED); + response.send(); + } catch (UsernameExistsException const& e) { + std::string error = "username exists"; + response.setContentLength(error.length()); + response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); + response.send() << error; + } catch (EmailExistsException const& e) { + std::string error = "email exists"; + response.setContentLength(error.length()); + response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); + response.send() << error; + } catch (Poco::LogicException const& e) { + response.setContentLength(e.message().length()); + response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); + response.send() << e.message(); + } catch (Poco::JSON::JSONException const& e) { + response.setContentLength(e.message().length()); + response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); + response.send() << e.message(); + } catch (Poco::Exception const& e) { + response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); response.send(); + logger.error( + "[ERROR] POST /api/register: Exception %s:\n%s\n", + std::string(e.className()), + e.displayText() + ); } - } catch (UsernameExistsException const& e) { - std::string error = "username exists"; - response.setContentLength(error.length()); - response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); - response.send() << error; - } catch (EmailExistsException const& e) { - std::string error = "email exists"; - response.setContentLength(error.length()); - response.setStatusAndReason(HTTPResponse::HTTP_CONFLICT); - response.send() << error; - } catch (Poco::LogicException const& e) { - response.setContentLength(e.message().length()); - response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); - response.send() << e.message(); - } catch (Poco::JSON::JSONException const& e) { - response.setContentLength(e.message().length()); - response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); - response.send() << e.message(); - } catch (Poco::Exception const& e) { - response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); - response.send(); - logger.error( - "[ERROR] POST /api/register: Exception %s:\n%s\n", - std::string(e.className()), - e.displayText() - ); } } diff --git a/auth/src/app.cpp b/auth/src/app.cpp index 3db7b0ff..f3d9c190 100644 --- a/auth/src/app.cpp +++ b/auth/src/app.cpp @@ -2,6 +2,7 @@ #include "api/AuthRequestHandlerFactory.hpp" #include "api/DBConnector.hpp" +#include "api/TokenManager.hpp" #include #include @@ -32,11 +33,13 @@ int AppAuthServer::main(std::vector< std::string > const& args) { ); DBConnector db(dbSession); + TokenManager tokenManager; + Poco::UInt16 port = config().getUInt16("HTTP.port"); ServerSocket socket(port); HTTPServer httpServer( - new AuthRequestHandlerFactory(db), socket, new HTTPServerParams + new AuthRequestHandlerFactory(db, tokenManager), socket, new HTTPServerParams ); httpServer.start(); From b0ac4044486b43f66dbe4e040c0b9dc9decd2b76 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 14:02:26 +0000 Subject: [PATCH 014/152] Automated formatting --- auth/src/api/AuthRequestHandlerFactory.cpp | 9 +- auth/src/api/AuthRequestHandlerFactory.hpp | 2 +- auth/src/api/DBConnector.cpp | 28 ++-- auth/src/api/DBConnector.hpp | 2 +- auth/src/api/TokenManager.cpp | 50 +++---- auth/src/api/TokenManager.hpp | 13 +- auth/src/api/User.hpp | 8 +- auth/src/api/UserCredentials.hpp | 2 +- auth/src/api/handlers/LoginHandler.cpp | 131 ++++++++++-------- auth/src/api/handlers/LoginHandler.hpp | 14 +- auth/src/api/handlers/RegistrationHandler.cpp | 30 ++-- auth/src/app.cpp | 4 +- 12 files changed, 159 insertions(+), 134 deletions(-) diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp index f777a50e..0a840471 100644 --- a/auth/src/api/AuthRequestHandlerFactory.cpp +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -2,9 +2,9 @@ #include "DBConnector.hpp" #include "TokenManager.hpp" +#include "handlers/LoginHandler.hpp" #include "handlers/NotFoundHandler.hpp" #include "handlers/RegistrationHandler.hpp" -#include "handlers/LoginHandler.hpp" #include #include @@ -12,8 +12,11 @@ using Poco::Net::HTTPRequestHandler; using Poco::Net::HTTPServerRequest; -AuthRequestHandlerFactory::AuthRequestHandlerFactory(DBConnector& db, TokenManager& tokenManager) - : m_db(db), m_tokenManager(tokenManager) {} +AuthRequestHandlerFactory::AuthRequestHandlerFactory( + DBConnector& db, TokenManager& tokenManager +) + : m_db(db) + , m_tokenManager(tokenManager) {} HTTPRequestHandler* AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request diff --git a/auth/src/api/AuthRequestHandlerFactory.hpp b/auth/src/api/AuthRequestHandlerFactory.hpp index 3744a6d9..d18724a2 100644 --- a/auth/src/api/AuthRequestHandlerFactory.hpp +++ b/auth/src/api/AuthRequestHandlerFactory.hpp @@ -17,6 +17,6 @@ class AuthRequestHandlerFactory : public HTTPRequestHandlerFactory { ) override; private: - DBConnector& m_db; + DBConnector& m_db; TokenManager& m_tokenManager; }; diff --git a/auth/src/api/DBConnector.cpp b/auth/src/api/DBConnector.cpp index 89836bf0..d477d074 100644 --- a/auth/src/api/DBConnector.cpp +++ b/auth/src/api/DBConnector.cpp @@ -2,17 +2,17 @@ #include "UserCredentials.hpp" +#include +#include +#include #include #include +#include #include #include #include #include #include -#include -#include -#include -#include using Poco::Data::Statement; using namespace Poco::Data::Keywords; @@ -53,20 +53,22 @@ void DBConnector::createUser(UserCredentials user) { } } -UserCredentials DBConnector::findUserWithCredentials(const std::string& login) { - Statement sql(m_db); - std::string clogin = login; +UserCredentials DBConnector::findUserWithCredentials(std::string const& login) { + Statement sql(m_db); + std::string clogin = login; UserCredentials user; try { - sql << "SELECT id, username, email, name, created_at, password_hash, salt FROM users WHERE username = $1 OR email = $1", into(user), use(clogin); + sql << "SELECT id, username, email, name, created_at, password_hash, salt " + "FROM users WHERE username = $1 OR email = $1", + into(user), use(clogin); sql.execute(); - } catch (const Poco::Exception& e) { + } catch (Poco::Exception const& e) { Poco::Util::Application::instance().logger().error( - "[ERROR] DBConnector::findUser: Exception %s:\n%s", - std::string(e.className()), - e.displayText() + "[ERROR] DBConnector::findUser: Exception %s:\n%s", + std::string(e.className()), + e.displayText() ); throw; } @@ -171,4 +173,4 @@ namespace Poco::Data { TypeHandler< std::string >::extract(pos++, obj.salt, defVal.salt, pExt); } }; -} // namespace Poco::Data \ No newline at end of file +} // namespace Poco::Data diff --git a/auth/src/api/DBConnector.hpp b/auth/src/api/DBConnector.hpp index 9a5b3579..14e44721 100644 --- a/auth/src/api/DBConnector.hpp +++ b/auth/src/api/DBConnector.hpp @@ -13,7 +13,7 @@ class DBConnector { void createUser(UserCredentials user); - UserCredentials findUserWithCredentials(const std::string& login); + UserCredentials findUserWithCredentials(std::string const& login); private: Session& m_db; diff --git a/auth/src/api/TokenManager.cpp b/auth/src/api/TokenManager.cpp index a6d4a0c3..2aca4648 100644 --- a/auth/src/api/TokenManager.cpp +++ b/auth/src/api/TokenManager.cpp @@ -1,44 +1,46 @@ #include "TokenManager.hpp" + #include "PasswordHasher.hpp" #include "User.hpp" + #include #include #include #include #include -using Poco::JWT::Token; using Poco::JSON::Object; using Poco::JSON::Stringifier; +using Poco::JWT::Token; TokenManager::TokenManager() { - PasswordHasher hasher; - m_signer.setHMACKey(hasher.genSalt()); + PasswordHasher hasher; + m_signer.setHMACKey(hasher.genSalt()); } -std::string TokenManager::generate(const User& user) { - Token refresh, access; - Object payload; - Object ret; - std::stringstream tmp; +std::string TokenManager::generate(User const& user) { + Token refresh, access; + Object payload; + Object ret; + std::stringstream tmp; - payload.set("id", user.id); - payload.set("username", user.username); - payload.set("email", user.email); - payload.set("name", user.name); - payload.set("createdAt", user.createdAt); + payload.set("id", user.id); + payload.set("username", user.username); + payload.set("email", user.email); + payload.set("name", user.name); + payload.set("createdAt", user.createdAt); - refresh.payload() = payload; - refresh.setSubject("VCD JWT Refresh"); - refresh.setIssuedAt(Poco::Timestamp()); + refresh.payload() = payload; + refresh.setSubject("VCD JWT Refresh"); + refresh.setIssuedAt(Poco::Timestamp()); - access.payload() = payload; - access.setSubject("VCD JWT Access"); - access.setIssuedAt(Poco::Timestamp()); + access.payload() = payload; + access.setSubject("VCD JWT Access"); + access.setIssuedAt(Poco::Timestamp()); - ret.set("refresh", m_signer.sign(refresh, Signer::ALGO_HS256)); - ret.set("access", m_signer.sign(access, Signer::ALGO_HS256)); + ret.set("refresh", m_signer.sign(refresh, Signer::ALGO_HS256)); + ret.set("access", m_signer.sign(access, Signer::ALGO_HS256)); - Stringifier::stringify(ret, tmp); - return tmp.rdbuf()->str(); -} \ No newline at end of file + Stringifier::stringify(ret, tmp); + return tmp.rdbuf()->str(); +} diff --git a/auth/src/api/TokenManager.hpp b/auth/src/api/TokenManager.hpp index 4465f14e..33af293b 100644 --- a/auth/src/api/TokenManager.hpp +++ b/auth/src/api/TokenManager.hpp @@ -1,16 +1,19 @@ #pragma once +#include "User.hpp" + #include #include -#include "User.hpp" using Poco::JWT::Signer; class TokenManager { public: - TokenManager(); + TokenManager(); + public: - std::string generate(const User& user); + std::string generate(User const& user); + private: - Signer m_signer; -}; \ No newline at end of file + Signer m_signer; +}; diff --git a/auth/src/api/User.hpp b/auth/src/api/User.hpp index 55a0afd4..2c9f8374 100644 --- a/auth/src/api/User.hpp +++ b/auth/src/api/User.hpp @@ -3,9 +3,9 @@ struct User { public: - int id = -1; - std::string username = ""; - std::string email = ""; - std::string name = ""; + int id = -1; + std::string username = ""; + std::string email = ""; + std::string name = ""; std::string createdAt = ""; }; diff --git a/auth/src/api/UserCredentials.hpp b/auth/src/api/UserCredentials.hpp index 34e4d56c..ee06c1af 100644 --- a/auth/src/api/UserCredentials.hpp +++ b/auth/src/api/UserCredentials.hpp @@ -5,5 +5,5 @@ struct UserCredentials : public User { public: std::string passwordHash = ""; - std::string salt = ""; + std::string salt = ""; }; diff --git a/auth/src/api/handlers/LoginHandler.cpp b/auth/src/api/handlers/LoginHandler.cpp index 52364b46..b283ba45 100644 --- a/auth/src/api/handlers/LoginHandler.cpp +++ b/auth/src/api/handlers/LoginHandler.cpp @@ -1,7 +1,11 @@ #include "LoginHandler.hpp" +#include "../PasswordHasher.hpp" +#include "../TokenManager.hpp" + #include #include +#include #include #include #include @@ -10,74 +14,81 @@ #include #include #include -#include "../PasswordHasher.hpp" -#include "../TokenManager.hpp" -#include -using Poco::Net::HTTPServerRequest; -using Poco::Net::HTTPServerResponse; -using Poco::Net::HTTPResponse; -using Poco::JSON::Parser; +using Poco::Logger; using Poco::Dynamic::Var; using Poco::JSON::Object; -using Poco::Logger; +using Poco::JSON::Parser; +using Poco::Net::HTTPResponse; +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; -LoginHandler::LoginHandler(DBConnector& db, TokenManager& tokenManager): m_db(db), m_tokenManager(tokenManager) {} +LoginHandler::LoginHandler(DBConnector& db, TokenManager& tokenManager) + : m_db(db) + , m_tokenManager(tokenManager) {} -void LoginHandler::handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) { - Logger& logger = Poco::Util::Application::instance().logger(); - logger.information("POST /api/login from %s", request.clientAddress().toString()); +void LoginHandler::handleRequest( + HTTPServerRequest& request, HTTPServerResponse& response +) { + Logger& logger = Poco::Util::Application::instance().logger(); + logger.information( + "POST /api/login from %s", request.clientAddress().toString() + ); - if (request.getContentType() != "application/json") { - response.set("Accept-Post", "application/json; charset=UTF-8"); - response.setStatusAndReason(HTTPResponse::HTTP_UNSUPPORTED_MEDIA_TYPE); - response.send(); - } else if (!request.hasContentLength()) { - response.setStatusAndReason(HTTPResponse::HTTP_LENGTH_REQUIRED); - response.send(); - } else { - try { - std::string login; - std::string password; - UserCredentials user; + if (request.getContentType() != "application/json") { + response.set("Accept-Post", "application/json; charset=UTF-8"); + response.setStatusAndReason(HTTPResponse::HTTP_UNSUPPORTED_MEDIA_TYPE); + response.send(); + } else if (!request.hasContentLength()) { + response.setStatusAndReason(HTTPResponse::HTTP_LENGTH_REQUIRED); + response.send(); + } else { + try { + std::string login; + std::string password; + UserCredentials user; - PasswordHasher hasher; + PasswordHasher hasher; - Parser parser; - Var result = parser.parse(request.stream()); - Object::Ptr JSONObject = result.extract(); - login = JSONObject->getValue("login"); - password = JSONObject->getValue("password"); + Parser parser; + Var result = parser.parse(request.stream()); + Object::Ptr JSONObject = result.extract< Object::Ptr >(); + login = JSONObject->getValue< std::string >("login"); + password = JSONObject->getValue< std::string >("password"); - user = m_db.findUserWithCredentials(login); + user = m_db.findUserWithCredentials(login); - if (hasher.verifyPassword(password, user.passwordHash, user.salt)) { - std::string tokens = m_tokenManager.generate(user); - response.setContentType("application/json"); - response.setContentLength(tokens.length()); - response.setStatusAndReason(HTTPResponse::HTTP_OK); - response.send() << tokens; - } else { - response.set("WWW-Authenticate", "Bearer"); - response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); - response.send(); - } - } catch (const UserNotFoundException& e) { - response.set("WWW-Authenticate", "Bearer"); - response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); - response.send(); - } catch (Poco::LogicException const& e) { - response.setContentLength(e.message().length()); - response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); - response.send() << e.message(); - } catch (Poco::JSON::JSONException const& e) { - response.setContentLength(e.message().length()); - response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); - response.send() << e.message(); - } catch (const Poco::Exception& e) { - response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); - response.send(); - logger.error("[ERROR] POST /api/login: Exception %s:\n%s", std::string(e.className()), e.displayText()); - } + if (hasher.verifyPassword(password, user.passwordHash, user.salt)) { + std::string tokens = m_tokenManager.generate(user); + response.setContentType("application/json"); + response.setContentLength(tokens.length()); + response.setStatusAndReason(HTTPResponse::HTTP_OK); + response.send() << tokens; + } else { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } + } catch (UserNotFoundException const& e) { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } catch (Poco::LogicException const& e) { + response.setContentLength(e.message().length()); + response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); + response.send() << e.message(); + } catch (Poco::JSON::JSONException const& e) { + response.setContentLength(e.message().length()); + response.setStatusAndReason(HTTPResponse::HTTP_BAD_REQUEST); + response.send() << e.message(); + } catch (Poco::Exception const& e) { + response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); + response.send(); + logger.error( + "[ERROR] POST /api/login: Exception %s:\n%s", + std::string(e.className()), + e.displayText() + ); } -} \ No newline at end of file + } +} diff --git a/auth/src/api/handlers/LoginHandler.hpp b/auth/src/api/handlers/LoginHandler.hpp index a0bb5924..7815bd81 100644 --- a/auth/src/api/handlers/LoginHandler.hpp +++ b/auth/src/api/handlers/LoginHandler.hpp @@ -1,21 +1,23 @@ #pragma once -#include #include "../DBConnector.hpp" #include "../TokenManager.hpp" +#include + using Poco::Net::HTTPRequestHandler; using Poco::Net::HTTPServerRequest; using Poco::Net::HTTPServerResponse; class LoginHandler : public HTTPRequestHandler { public: - LoginHandler(DBConnector& m_db, TokenManager& tokenManager); + LoginHandler(DBConnector& m_db, TokenManager& tokenManager); public: - void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) override; + void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) + override; private: - DBConnector& m_db; - TokenManager& m_tokenManager; -}; \ No newline at end of file + DBConnector& m_db; + TokenManager& m_tokenManager; +}; diff --git a/auth/src/api/handlers/RegistrationHandler.cpp b/auth/src/api/handlers/RegistrationHandler.cpp index 2fbec018..b4c7c818 100644 --- a/auth/src/api/handlers/RegistrationHandler.cpp +++ b/auth/src/api/handlers/RegistrationHandler.cpp @@ -44,24 +44,24 @@ void RegistrationHandler::handleRequest( response.send(); } else { try { - Parser parser; - PasswordHasher hasher; - UserCredentials newUser; - Var result = parser.parse(request.stream()); - Object::Ptr JSONObject = result.extract< Object::Ptr >(); + Parser parser; + PasswordHasher hasher; + UserCredentials newUser; + Var result = parser.parse(request.stream()); + Object::Ptr JSONObject = result.extract< Object::Ptr >(); - newUser.name = JSONObject->getValue< std::string >("name"); - newUser.username = JSONObject->getValue< std::string >("username"); - newUser.email = JSONObject->getValue< std::string >("email"); - newUser.salt = hasher.genSalt(); - newUser.passwordHash = hasher.encryptPassword( - JSONObject->getValue< std::string >("password"), newUser.salt - ); + newUser.name = JSONObject->getValue< std::string >("name"); + newUser.username = JSONObject->getValue< std::string >("username"); + newUser.email = JSONObject->getValue< std::string >("email"); + newUser.salt = hasher.genSalt(); + newUser.passwordHash = hasher.encryptPassword( + JSONObject->getValue< std::string >("password"), newUser.salt + ); - m_db.createUser(newUser); + m_db.createUser(newUser); - response.setStatusAndReason(HTTPResponse::HTTP_CREATED); - response.send(); + response.setStatusAndReason(HTTPResponse::HTTP_CREATED); + response.send(); } catch (UsernameExistsException const& e) { std::string error = "username exists"; response.setContentLength(error.length()); diff --git a/auth/src/app.cpp b/auth/src/app.cpp index f3d9c190..1e119ef3 100644 --- a/auth/src/app.cpp +++ b/auth/src/app.cpp @@ -39,7 +39,9 @@ int AppAuthServer::main(std::vector< std::string > const& args) { ServerSocket socket(port); HTTPServer httpServer( - new AuthRequestHandlerFactory(db, tokenManager), socket, new HTTPServerParams + new AuthRequestHandlerFactory(db, tokenManager), + socket, + new HTTPServerParams ); httpServer.start(); From a433936d0bb31499c1c27f1e670cb0b0354b40a5 Mon Sep 17 00:00:00 2001 From: arsenez Date: Mon, 30 Jun 2025 17:43:34 +0300 Subject: [PATCH 015/152] Fix poco postgre module in nix --- auth/default.nix | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/auth/default.nix b/auth/default.nix index dd2a14ee..2b5a8e8e 100644 --- a/auth/default.nix +++ b/auth/default.nix @@ -1,13 +1,23 @@ with import {}; +let + # Переопределяем Poco с поддержкой PostgreSQL + pocoWithPostgres = pkgs.poco.overrideAttrs (oldAttrs: { + buildInputs = (oldAttrs.buildInputs or []) ++ [ pkgs.postgresql ]; + cmakeFlags = (oldAttrs.cmakeFlags or []) ++ [ + (pkgs.lib.cmakeBool "POCO_ENABLE_DATA_POSTGRESQL" true) + ]; + }); +in + stdenv.mkDerivation { name = "VCDAuth"; src = ./.; nativeBuildInputs = [ cmake ninja ]; - buildInputs = [ poco ]; + buildInputs = [ pocoWithPostgres postgresql ]; configurePhase = "cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release"; buildPhase = "cmake --build build --config Release"; installPhase = "install -D build/VCDAuth $out/bin/VCDAuth"; -} \ No newline at end of file +} From 3a9758f691bf4d57f4e2262bf1a74ba6bcc0a66f Mon Sep 17 00:00:00 2001 From: arsenez Date: Mon, 30 Jun 2025 18:22:24 +0300 Subject: [PATCH 016/152] Remove unnecessary includes --- auth/src/api/PasswordHasher.cpp | 1 - auth/src/api/PasswordHasher.hpp | 2 -- 2 files changed, 3 deletions(-) diff --git a/auth/src/api/PasswordHasher.cpp b/auth/src/api/PasswordHasher.cpp index f93925f5..14de3e61 100644 --- a/auth/src/api/PasswordHasher.cpp +++ b/auth/src/api/PasswordHasher.cpp @@ -5,7 +5,6 @@ #include #include #include -#include using Poco::DigestEngine; using Poco::HMACEngine; diff --git a/auth/src/api/PasswordHasher.hpp b/auth/src/api/PasswordHasher.hpp index 2857d204..7eb6cc64 100644 --- a/auth/src/api/PasswordHasher.hpp +++ b/auth/src/api/PasswordHasher.hpp @@ -1,7 +1,5 @@ #pragma once #include -#include -#include #include class PasswordHasher { From ded977b50da96e4c901cf3bd34eba82b0fde4191 Mon Sep 17 00:00:00 2001 From: arsenez Date: Mon, 30 Jun 2025 19:16:58 +0300 Subject: [PATCH 017/152] Add methods for verifying tokens --- auth/src/api/TokenManager.cpp | 35 ++++++++++++++++++++++++++++++++++- auth/src/api/TokenManager.hpp | 15 ++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/auth/src/api/TokenManager.cpp b/auth/src/api/TokenManager.cpp index 2aca4648..68ce4148 100644 --- a/auth/src/api/TokenManager.cpp +++ b/auth/src/api/TokenManager.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include using Poco::JSON::Object; using Poco::JSON::Stringifier; @@ -16,9 +18,10 @@ using Poco::JWT::Token; TokenManager::TokenManager() { PasswordHasher hasher; m_signer.setHMACKey(hasher.genSalt()); + Poco::Util::Application::instance().logger().information("JWT key: %s", m_signer.getHMACKey()); } -std::string TokenManager::generate(User const& user) { +std::string TokenManager::generate(User const& user) const { Token refresh, access; Object payload; Object ret; @@ -31,10 +34,12 @@ std::string TokenManager::generate(User const& user) { payload.set("createdAt", user.createdAt); refresh.payload() = payload; + refresh.setType("JWT"); refresh.setSubject("VCD JWT Refresh"); refresh.setIssuedAt(Poco::Timestamp()); access.payload() = payload; + access.setType("JWT"); access.setSubject("VCD JWT Access"); access.setIssuedAt(Poco::Timestamp()); @@ -44,3 +49,31 @@ std::string TokenManager::generate(User const& user) { Stringifier::stringify(ret, tmp); return tmp.rdbuf()->str(); } + +bool TokenManager::verify(const std::string& token, Type type) const { + Token decoded; + if (m_signer.tryVerify(token, decoded) && !decoded.getIssuedAt().isElapsed(15LL * 60000000LL)) { + switch (type) { + case ACCESS: return decoded.getSubject() == "VCD JWT Access"; + case REFRESH: return decoded.getSubject() == "VCD JWT Refresh"; + default: return true; + } + } else { + return false; + } +} + +User TokenManager::getUser(const std::string& token) const { + User ret; + Token decoded; + + if (m_signer.tryVerify(token, decoded)) { + ret.id = decoded.payload().getValue("id"); + ret.username = decoded.payload().getValue("username"); + ret.email = decoded.payload().getValue("email"); + ret.name = decoded.payload().getValue("name"); + ret.createdAt = decoded.payload().getValue("createdAt"); + } + + return ret; +} \ No newline at end of file diff --git a/auth/src/api/TokenManager.hpp b/auth/src/api/TokenManager.hpp index 33af293b..b8391716 100644 --- a/auth/src/api/TokenManager.hpp +++ b/auth/src/api/TokenManager.hpp @@ -3,16 +3,29 @@ #include "User.hpp" #include +#include #include using Poco::JWT::Signer; +namespace Poco::JWT {} + class TokenManager { +public: + enum Type { + ACCESS, + REFRESH, + ANY + }; public: TokenManager(); public: - std::string generate(User const& user); + std::string generate(User const& user) const; + + bool verify(const std::string& token, Type type = ANY) const; + + User getUser(const std::string& token) const; private: Signer m_signer; From 38e23b249583a82b2b873ceb10576f44fd7bcdda Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:17:58 +0000 Subject: [PATCH 018/152] Automated formatting --- auth/src/api/TokenManager.cpp | 25 ++++++++++++++----------- auth/src/api/TokenManager.hpp | 11 ++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/auth/src/api/TokenManager.cpp b/auth/src/api/TokenManager.cpp index 68ce4148..866ebc7d 100644 --- a/auth/src/api/TokenManager.cpp +++ b/auth/src/api/TokenManager.cpp @@ -18,7 +18,9 @@ using Poco::JWT::Token; TokenManager::TokenManager() { PasswordHasher hasher; m_signer.setHMACKey(hasher.genSalt()); - Poco::Util::Application::instance().logger().information("JWT key: %s", m_signer.getHMACKey()); + Poco::Util::Application::instance().logger().information( + "JWT key: %s", m_signer.getHMACKey() + ); } std::string TokenManager::generate(User const& user) const { @@ -50,9 +52,10 @@ std::string TokenManager::generate(User const& user) const { return tmp.rdbuf()->str(); } -bool TokenManager::verify(const std::string& token, Type type) const { +bool TokenManager::verify(std::string const& token, Type type) const { Token decoded; - if (m_signer.tryVerify(token, decoded) && !decoded.getIssuedAt().isElapsed(15LL * 60000000LL)) { + if (m_signer.tryVerify(token, decoded) && + !decoded.getIssuedAt().isElapsed(15LL * 60000000LL)) { switch (type) { case ACCESS: return decoded.getSubject() == "VCD JWT Access"; case REFRESH: return decoded.getSubject() == "VCD JWT Refresh"; @@ -63,17 +66,17 @@ bool TokenManager::verify(const std::string& token, Type type) const { } } -User TokenManager::getUser(const std::string& token) const { - User ret; +User TokenManager::getUser(std::string const& token) const { + User ret; Token decoded; if (m_signer.tryVerify(token, decoded)) { - ret.id = decoded.payload().getValue("id"); - ret.username = decoded.payload().getValue("username"); - ret.email = decoded.payload().getValue("email"); - ret.name = decoded.payload().getValue("name"); - ret.createdAt = decoded.payload().getValue("createdAt"); + ret.id = decoded.payload().getValue< int >("id"); + ret.username = decoded.payload().getValue< std::string >("username"); + ret.email = decoded.payload().getValue< std::string >("email"); + ret.name = decoded.payload().getValue< std::string >("name"); + ret.createdAt = decoded.payload().getValue< std::string >("createdAt"); } return ret; -} \ No newline at end of file +} diff --git a/auth/src/api/TokenManager.hpp b/auth/src/api/TokenManager.hpp index b8391716..da589aef 100644 --- a/auth/src/api/TokenManager.hpp +++ b/auth/src/api/TokenManager.hpp @@ -12,20 +12,17 @@ namespace Poco::JWT {} class TokenManager { public: - enum Type { - ACCESS, - REFRESH, - ANY - }; + enum Type { ACCESS, REFRESH, ANY }; + public: TokenManager(); public: std::string generate(User const& user) const; - bool verify(const std::string& token, Type type = ANY) const; + bool verify(std::string const& token, Type type = ANY) const; - User getUser(const std::string& token) const; + User getUser(std::string const& token) const; private: Signer m_signer; From c912405a6f2bcfb6c802fc22aa5b2cef7d5c344d Mon Sep 17 00:00:00 2001 From: witch2256 Date: Wed, 2 Jul 2025 19:35:09 +0300 Subject: [PATCH 019/152] start of login page --- UI/src/App.jsx | 3 +++ UI/src/pages/mainPage.jsx | 9 ++++++++- UI/src/pages/profile.jsx | 1 - 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/UI/src/App.jsx b/UI/src/App.jsx index f862f7a8..cfe699a6 100644 --- a/UI/src/App.jsx +++ b/UI/src/App.jsx @@ -1,5 +1,6 @@ import Main from "./pages/mainPage.jsx"; import Profile from "./pages/profile.jsx"; +import Auth from "./pages/auth.jsx"; import "@xyflow/react/dist/style.css"; @@ -11,6 +12,7 @@ import "./CSS/dnd.css"; import "./CSS/backdrop.css"; import "./CSS/circuitsMenu.css"; import "./CSS/contextMenu.css"; +import "./CSS/auth.css" import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; @@ -23,6 +25,7 @@ function App() { } /> } /> + } /> diff --git a/UI/src/pages/mainPage.jsx b/UI/src/pages/mainPage.jsx index 909de5cd..fd06c8ea 100644 --- a/UI/src/pages/mainPage.jsx +++ b/UI/src/pages/mainPage.jsx @@ -555,7 +555,14 @@ export default function Main() { SVGClassName="openSettingsButtonIcon" draggable="false" /> - + + + Log in +
{ From d25633cc72f87db338e51e68ab6768d81692606e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:35:41 +0000 Subject: [PATCH 020/152] Automated formatting --- UI/src/App.jsx | 2 +- UI/src/pages/mainPage.jsx | 57 +++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/UI/src/App.jsx b/UI/src/App.jsx index cfe699a6..e6d6ff6d 100644 --- a/UI/src/App.jsx +++ b/UI/src/App.jsx @@ -12,7 +12,7 @@ import "./CSS/dnd.css"; import "./CSS/backdrop.css"; import "./CSS/circuitsMenu.css"; import "./CSS/contextMenu.css"; -import "./CSS/auth.css" +import "./CSS/auth.css"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; diff --git a/UI/src/pages/mainPage.jsx b/UI/src/pages/mainPage.jsx index fd06c8ea..95ae9bbd 100644 --- a/UI/src/pages/mainPage.jsx +++ b/UI/src/pages/mainPage.jsx @@ -117,19 +117,18 @@ export default function Main() { useEffect(() => { const saved = localStorage.getItem("userSettings"); if (saved) { - const parsed = JSON.parse(saved); - if (parsed.currentBG) setCurrentBG(parsed.currentBG); - if (typeof parsed.showMinimap === "boolean") - setShowMinimap(parsed.showMinimap); - if (parsed.theme) setTheme(parsed.theme); - if (parsed.activeAction) setActiveAction(parsed.activeAction); - if (parsed.activeWire) setActiveWire(parsed.activeWire); - if (parsed.activeButton) setActiveButton(parsed.activeButton); - if (typeof parsed.openSettings === "boolean") - setOpenSettings(parsed.openSettings); - if (typeof parsed.circuitsMenuState === "boolean") - setCircuitsMenuState(parsed.circuitsMenuState); - + const parsed = JSON.parse(saved); + if (parsed.currentBG) setCurrentBG(parsed.currentBG); + if (typeof parsed.showMinimap === "boolean") + setShowMinimap(parsed.showMinimap); + if (parsed.theme) setTheme(parsed.theme); + if (parsed.activeAction) setActiveAction(parsed.activeAction); + if (parsed.activeWire) setActiveWire(parsed.activeWire); + if (parsed.activeButton) setActiveButton(parsed.activeButton); + if (typeof parsed.openSettings === "boolean") + setOpenSettings(parsed.openSettings); + if (typeof parsed.circuitsMenuState === "boolean") + setCircuitsMenuState(parsed.circuitsMenuState); } }, []); @@ -161,22 +160,22 @@ export default function Main() { useEffect(() => { const handleKeyDown = (e) => { //deleting by clicking delete/backspace(delete for windows and macOS, backspace for windows) - if (e.key === 'Delete' || e.key === 'Backspace') { + if (e.key === "Delete" || e.key === "Backspace") { e.preventDefault(); const currentNodes = nodesRef.current; const currentEdges = edgesRef.current; - const selectedNodes = currentNodes.filter(node => node.selected); - const selectedEdges = currentEdges.filter(edge => edge.selected); + const selectedNodes = currentNodes.filter((node) => node.selected); + const selectedEdges = currentEdges.filter((edge) => edge.selected); if (selectedNodes.length === 0 && selectedEdges.length === 0) return; - const nodeIdsToRemove = selectedNodes.map(node => node.id); + const nodeIdsToRemove = selectedNodes.map((node) => node.id); const newNodes = currentNodes.filter( - node => !nodeIdsToRemove.includes(node.id) + (node) => !nodeIdsToRemove.includes(node.id), ); const newEdges = currentEdges.filter( - edge => - !selectedEdges.some(selected => selected.id === edge.id) && - !nodeIdsToRemove.includes(edge.source) && - !nodeIdsToRemove.includes(edge.target) + (edge) => + !selectedEdges.some((selected) => selected.id === edge.id) && + !nodeIdsToRemove.includes(edge.source) && + !nodeIdsToRemove.includes(edge.target), ); setNodes(newNodes); setEdges(newEdges); @@ -227,11 +226,9 @@ export default function Main() { if (e.key === "Escape" && openSettings) { setOpenSettings(false); } - - }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); }, [openSettings]); //Sets current theme to the whole document (наверное) @@ -555,11 +552,11 @@ export default function Main() { SVGClassName="openSettingsButtonIcon" draggable="false" /> - + Log in From cb985f2b1fb71dbbc821a913f3273291250e23f7 Mon Sep 17 00:00:00 2001 From: arsenez Date: Wed, 2 Jul 2025 23:28:23 +0300 Subject: [PATCH 021/152] Implement POST /api/auth/verify --- auth/src/api/AuthRequestHandlerFactory.cpp | 7 ++- auth/src/api/TokenManager.cpp | 27 ++++++---- auth/src/api/TokenManager.hpp | 5 ++ auth/src/api/handlers/LoginHandler.cpp | 4 +- auth/src/api/handlers/RegistrationHandler.cpp | 4 +- auth/src/api/handlers/VerificationHandler.cpp | 50 +++++++++++++++++++ auth/src/api/handlers/VerificationHandler.hpp | 21 ++++++++ 7 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 auth/src/api/handlers/VerificationHandler.cpp create mode 100644 auth/src/api/handlers/VerificationHandler.hpp diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp index 0a840471..93e03977 100644 --- a/auth/src/api/AuthRequestHandlerFactory.cpp +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -5,6 +5,7 @@ #include "handlers/LoginHandler.hpp" #include "handlers/NotFoundHandler.hpp" #include "handlers/RegistrationHandler.hpp" +#include "handlers/VerificationHandler.hpp" #include #include @@ -22,10 +23,12 @@ HTTPRequestHandler* AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request ) { if (request.getMethod() == "POST") { - if (request.getURI() == "/api/register") { + if (request.getURI() == "/api/auth/register") { return new RegistrationHandler(m_db); - } else if (request.getURI() == "/api/login") { + } else if (request.getURI() == "/api/auth/login") { return new LoginHandler(m_db, m_tokenManager); + } else if (request.getURI() == "/api/auth/verify") { + return new VerificationHandler(m_db, m_tokenManager); } } return new NotFoundHandler; diff --git a/auth/src/api/TokenManager.cpp b/auth/src/api/TokenManager.cpp index 866ebc7d..9322d94e 100644 --- a/auth/src/api/TokenManager.cpp +++ b/auth/src/api/TokenManager.cpp @@ -54,16 +54,7 @@ std::string TokenManager::generate(User const& user) const { bool TokenManager::verify(std::string const& token, Type type) const { Token decoded; - if (m_signer.tryVerify(token, decoded) && - !decoded.getIssuedAt().isElapsed(15LL * 60000000LL)) { - switch (type) { - case ACCESS: return decoded.getSubject() == "VCD JWT Access"; - case REFRESH: return decoded.getSubject() == "VCD JWT Refresh"; - default: return true; - } - } else { - return false; - } + return _verify(token, decoded, type); } User TokenManager::getUser(std::string const& token) const { @@ -80,3 +71,19 @@ User TokenManager::getUser(std::string const& token) const { return ret; } + +bool TokenManager::_verify(std::string const& token, Poco::JWT::Token& out, Type type) const { + if (m_signer.tryVerify(token, out)) { + Type realType = out.getSubject() == "VCD JWT Access" ? ACCESS : REFRESH; + if (type != ANY && realType != type) { + return false; + } + switch (realType) { + case ACCESS: return !out.getIssuedAt().isElapsed(15LL * 60000000LL); + case REFRESH: return !out.getIssuedAt().isElapsed(30LL * 24LL * 60LL * 60000000LL); + default: return false; + } + } else { + return false; + } +} \ No newline at end of file diff --git a/auth/src/api/TokenManager.hpp b/auth/src/api/TokenManager.hpp index da589aef..2fa870d0 100644 --- a/auth/src/api/TokenManager.hpp +++ b/auth/src/api/TokenManager.hpp @@ -24,6 +24,11 @@ class TokenManager { User getUser(std::string const& token) const; + std::string refresh(std::string const& token); + +private: + bool _verify(std::string const& token, Poco::JWT::Token& out, Type type = ANY) const; + private: Signer m_signer; }; diff --git a/auth/src/api/handlers/LoginHandler.cpp b/auth/src/api/handlers/LoginHandler.cpp index b283ba45..37d77d66 100644 --- a/auth/src/api/handlers/LoginHandler.cpp +++ b/auth/src/api/handlers/LoginHandler.cpp @@ -32,7 +32,7 @@ void LoginHandler::handleRequest( ) { Logger& logger = Poco::Util::Application::instance().logger(); logger.information( - "POST /api/login from %s", request.clientAddress().toString() + "POST /api/auth/login from %s", request.clientAddress().toString() ); if (request.getContentType() != "application/json") { @@ -85,7 +85,7 @@ void LoginHandler::handleRequest( response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); response.send(); logger.error( - "[ERROR] POST /api/login: Exception %s:\n%s", + "[ERROR] POST /api/auth/login: Exception %s:\n%s", std::string(e.className()), e.displayText() ); diff --git a/auth/src/api/handlers/RegistrationHandler.cpp b/auth/src/api/handlers/RegistrationHandler.cpp index b4c7c818..f0dfa297 100644 --- a/auth/src/api/handlers/RegistrationHandler.cpp +++ b/auth/src/api/handlers/RegistrationHandler.cpp @@ -32,7 +32,7 @@ void RegistrationHandler::handleRequest( ) { Logger& logger = Application::instance().logger(); logger.information( - "POST /api/register from %s", request.clientAddress().toString() + "POST /api/auth/register from %s", request.clientAddress().toString() ); if (!request.hasContentLength()) { @@ -84,7 +84,7 @@ void RegistrationHandler::handleRequest( response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); response.send(); logger.error( - "[ERROR] POST /api/register: Exception %s:\n%s\n", + "[ERROR] POST /api/auth/register: Exception %s:\n%s\n", std::string(e.className()), e.displayText() ); diff --git a/auth/src/api/handlers/VerificationHandler.cpp b/auth/src/api/handlers/VerificationHandler.cpp new file mode 100644 index 00000000..b27f2469 --- /dev/null +++ b/auth/src/api/handlers/VerificationHandler.cpp @@ -0,0 +1,50 @@ +#include "VerificationHandler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; +using Poco::Net::HTTPResponse; +using Poco::Logger; + +VerificationHandler::VerificationHandler(DBConnector& db, TokenManager& tokenManager): m_db(db), m_tokenManager(tokenManager) {} + +void VerificationHandler::handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) { + Logger& logger = Poco::Util::Application::instance().logger(); + logger.information("POST /api/auth/verify from %s", request.clientAddress().toString()); + + try { + std::regex regexp("^Bearer (.*)$"); + std::smatch match; + std::string bearer = request.get("Authorization"); + if (std::regex_match(bearer, match, regexp)) { + if (m_tokenManager.verify(match[1].str(), TokenManager::ACCESS)) { + response.setStatusAndReason(HTTPResponse::HTTP_OK); + response.send(); + } else { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } + } else { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } + } catch (const Poco::NotFoundException& e){ + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } catch (const Poco::Exception& e) { + response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); + response.send(); + logger.error("[ERROR] POST /api/auth/verify: Exception %s:%s", std::string(e.className()), e.displayText()); + } +} \ No newline at end of file diff --git a/auth/src/api/handlers/VerificationHandler.hpp b/auth/src/api/handlers/VerificationHandler.hpp new file mode 100644 index 00000000..8cad41dd --- /dev/null +++ b/auth/src/api/handlers/VerificationHandler.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "../DBConnector.hpp" +#include "../TokenManager.hpp" + +using Poco::Net::HTTPRequestHandler; +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; + +class VerificationHandler : public HTTPRequestHandler { +public: + VerificationHandler(DBConnector& db, TokenManager& tokenManager); + +public: + void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) override; + +private: + DBConnector& m_db; + TokenManager& m_tokenManager; +}; \ No newline at end of file From 57a5a99cc589067d00790df70e939b3a586c82ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:28:43 +0000 Subject: [PATCH 022/152] Automated formatting --- auth/src/api/TokenManager.cpp | 9 ++- auth/src/api/TokenManager.hpp | 3 +- auth/src/api/handlers/VerificationHandler.cpp | 75 +++++++++++-------- auth/src/api/handlers/VerificationHandler.hpp | 10 ++- 4 files changed, 58 insertions(+), 39 deletions(-) diff --git a/auth/src/api/TokenManager.cpp b/auth/src/api/TokenManager.cpp index 9322d94e..91a0015d 100644 --- a/auth/src/api/TokenManager.cpp +++ b/auth/src/api/TokenManager.cpp @@ -72,7 +72,9 @@ User TokenManager::getUser(std::string const& token) const { return ret; } -bool TokenManager::_verify(std::string const& token, Poco::JWT::Token& out, Type type) const { +bool TokenManager::_verify( + std::string const& token, Poco::JWT::Token& out, Type type +) const { if (m_signer.tryVerify(token, out)) { Type realType = out.getSubject() == "VCD JWT Access" ? ACCESS : REFRESH; if (type != ANY && realType != type) { @@ -80,10 +82,11 @@ bool TokenManager::_verify(std::string const& token, Poco::JWT::Token& out, Type } switch (realType) { case ACCESS: return !out.getIssuedAt().isElapsed(15LL * 60000000LL); - case REFRESH: return !out.getIssuedAt().isElapsed(30LL * 24LL * 60LL * 60000000LL); + case REFRESH: + return !out.getIssuedAt().isElapsed(30LL * 24LL * 60LL * 60000000LL); default: return false; } } else { return false; } -} \ No newline at end of file +} diff --git a/auth/src/api/TokenManager.hpp b/auth/src/api/TokenManager.hpp index 2fa870d0..05a50ef4 100644 --- a/auth/src/api/TokenManager.hpp +++ b/auth/src/api/TokenManager.hpp @@ -27,7 +27,8 @@ class TokenManager { std::string refresh(std::string const& token); private: - bool _verify(std::string const& token, Poco::JWT::Token& out, Type type = ANY) const; + bool _verify(std::string const& token, Poco::JWT::Token& out, Type type = ANY) + const; private: Signer m_signer; diff --git a/auth/src/api/handlers/VerificationHandler.cpp b/auth/src/api/handlers/VerificationHandler.cpp index b27f2469..5a57bf79 100644 --- a/auth/src/api/handlers/VerificationHandler.cpp +++ b/auth/src/api/handlers/VerificationHandler.cpp @@ -1,4 +1,5 @@ #include "VerificationHandler.hpp" + #include #include #include @@ -6,45 +7,57 @@ #include #include #include -#include #include +#include +using Poco::Logger; +using Poco::Net::HTTPResponse; using Poco::Net::HTTPServerRequest; using Poco::Net::HTTPServerResponse; -using Poco::Net::HTTPResponse; -using Poco::Logger; -VerificationHandler::VerificationHandler(DBConnector& db, TokenManager& tokenManager): m_db(db), m_tokenManager(tokenManager) {} +VerificationHandler::VerificationHandler( + DBConnector& db, TokenManager& tokenManager +) + : m_db(db) + , m_tokenManager(tokenManager) {} -void VerificationHandler::handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) { - Logger& logger = Poco::Util::Application::instance().logger(); - logger.information("POST /api/auth/verify from %s", request.clientAddress().toString()); +void VerificationHandler::handleRequest( + HTTPServerRequest& request, HTTPServerResponse& response +) { + Logger& logger = Poco::Util::Application::instance().logger(); + logger.information( + "POST /api/auth/verify from %s", request.clientAddress().toString() + ); - try { - std::regex regexp("^Bearer (.*)$"); - std::smatch match; - std::string bearer = request.get("Authorization"); - if (std::regex_match(bearer, match, regexp)) { - if (m_tokenManager.verify(match[1].str(), TokenManager::ACCESS)) { - response.setStatusAndReason(HTTPResponse::HTTP_OK); - response.send(); - } else { - response.set("WWW-Authenticate", "Bearer"); - response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); - response.send(); - } - } else { - response.set("WWW-Authenticate", "Bearer"); - response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); - response.send(); - } - } catch (const Poco::NotFoundException& e){ + try { + std::regex regexp("^Bearer (.*)$"); + std::smatch match; + std::string bearer = request.get("Authorization"); + if (std::regex_match(bearer, match, regexp)) { + if (m_tokenManager.verify(match[1].str(), TokenManager::ACCESS)) { + response.setStatusAndReason(HTTPResponse::HTTP_OK); + response.send(); + } else { response.set("WWW-Authenticate", "Bearer"); response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); response.send(); - } catch (const Poco::Exception& e) { - response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); - response.send(); - logger.error("[ERROR] POST /api/auth/verify: Exception %s:%s", std::string(e.className()), e.displayText()); + } + } else { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); } -} \ No newline at end of file + } catch (Poco::NotFoundException const& e) { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } catch (Poco::Exception const& e) { + response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); + response.send(); + logger.error( + "[ERROR] POST /api/auth/verify: Exception %s:%s", + std::string(e.className()), + e.displayText() + ); + } +} diff --git a/auth/src/api/handlers/VerificationHandler.hpp b/auth/src/api/handlers/VerificationHandler.hpp index 8cad41dd..45b29cc9 100644 --- a/auth/src/api/handlers/VerificationHandler.hpp +++ b/auth/src/api/handlers/VerificationHandler.hpp @@ -1,9 +1,10 @@ #pragma once -#include #include "../DBConnector.hpp" #include "../TokenManager.hpp" +#include + using Poco::Net::HTTPRequestHandler; using Poco::Net::HTTPServerRequest; using Poco::Net::HTTPServerResponse; @@ -13,9 +14,10 @@ class VerificationHandler : public HTTPRequestHandler { VerificationHandler(DBConnector& db, TokenManager& tokenManager); public: - void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) override; + void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) + override; private: - DBConnector& m_db; + DBConnector& m_db; TokenManager& m_tokenManager; -}; \ No newline at end of file +}; From cff6ec1cedc05c07660085397a40581b3992676f Mon Sep 17 00:00:00 2001 From: arsenez Date: Thu, 3 Jul 2025 09:32:38 +0300 Subject: [PATCH 023/152] Implement POST /api/auth/refresh --- auth/src/api/AuthRequestHandlerFactory.cpp | 5 +- auth/src/api/TokenManager.cpp | 4 ++ auth/src/api/handlers/RefreshHandler.cpp | 56 +++++++++++++++++++ auth/src/api/handlers/RefreshHandler.hpp | 18 ++++++ auth/src/api/handlers/VerificationHandler.cpp | 5 +- auth/src/api/handlers/VerificationHandler.hpp | 3 +- 6 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 auth/src/api/handlers/RefreshHandler.cpp create mode 100644 auth/src/api/handlers/RefreshHandler.hpp diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp index 93e03977..f47b3539 100644 --- a/auth/src/api/AuthRequestHandlerFactory.cpp +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -6,6 +6,7 @@ #include "handlers/NotFoundHandler.hpp" #include "handlers/RegistrationHandler.hpp" #include "handlers/VerificationHandler.hpp" +#include "handlers/RefreshHandler.hpp" #include #include @@ -28,7 +29,9 @@ AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request } else if (request.getURI() == "/api/auth/login") { return new LoginHandler(m_db, m_tokenManager); } else if (request.getURI() == "/api/auth/verify") { - return new VerificationHandler(m_db, m_tokenManager); + return new VerificationHandler(m_tokenManager); + } else if (request.getURI() == "/api/auth/refresh") { + return new RefreshHandler(m_tokenManager); } } return new NotFoundHandler; diff --git a/auth/src/api/TokenManager.cpp b/auth/src/api/TokenManager.cpp index 91a0015d..99ac14c5 100644 --- a/auth/src/api/TokenManager.cpp +++ b/auth/src/api/TokenManager.cpp @@ -72,6 +72,10 @@ User TokenManager::getUser(std::string const& token) const { return ret; } +std::string TokenManager::refresh(std::string const& token) { + return generate(getUser(token)); +} + bool TokenManager::_verify( std::string const& token, Poco::JWT::Token& out, Type type ) const { diff --git a/auth/src/api/handlers/RefreshHandler.cpp b/auth/src/api/handlers/RefreshHandler.cpp new file mode 100644 index 00000000..74b51754 --- /dev/null +++ b/auth/src/api/handlers/RefreshHandler.cpp @@ -0,0 +1,56 @@ +#include "RefreshHandler.hpp" +#include +#include +#include +#include +#include +#include + +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; +using Poco::Net::HTTPResponse; +using Poco::Logger; + +RefreshHandler::RefreshHandler(TokenManager& tokenManager): m_tokenManager(tokenManager) {} + +void RefreshHandler::handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) { + Logger& logger = Poco::Util::Application::instance().logger(); + logger.information( + "POST /api/auth/refresh from %s", request.clientAddress().toString() + ); + + try { + std::regex regexp("^Bearer (.*)$"); + std::smatch match; + std::string bearer = request.get("Authorization"); + if (std::regex_match(bearer, match, regexp)) { + if (m_tokenManager.verify(match[1].str(), TokenManager::REFRESH)) { + std::string tokens = m_tokenManager.refresh(match[1]); + response.setContentType("application/json"); + response.setContentLength(tokens.length()); + response.setStatusAndReason(HTTPResponse::HTTP_OK); + response.send() << tokens; + } else { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } + } else { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } + } catch (Poco::NotFoundException const& e) { + response.set("WWW-Authenticate", "Bearer"); + response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED); + response.send(); + } catch (Poco::Exception const& e) { + response.setStatusAndReason(HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); + response.send(); + logger.error( + "[ERROR] POST /api/auth/refresh: Exception %s:%s", + std::string(e.className()), + e.displayText() + ); + } +} \ No newline at end of file diff --git a/auth/src/api/handlers/RefreshHandler.hpp b/auth/src/api/handlers/RefreshHandler.hpp new file mode 100644 index 00000000..7923291b --- /dev/null +++ b/auth/src/api/handlers/RefreshHandler.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include "../TokenManager.hpp" +using Poco::Net::HTTPRequestHandler; +using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; + +class RefreshHandler : public HTTPRequestHandler { +public: + RefreshHandler(TokenManager& tokenManager); + +public: + void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) override; + +private: + TokenManager& m_tokenManager; +}; \ No newline at end of file diff --git a/auth/src/api/handlers/VerificationHandler.cpp b/auth/src/api/handlers/VerificationHandler.cpp index 5a57bf79..dcb1babe 100644 --- a/auth/src/api/handlers/VerificationHandler.cpp +++ b/auth/src/api/handlers/VerificationHandler.cpp @@ -16,10 +16,9 @@ using Poco::Net::HTTPServerRequest; using Poco::Net::HTTPServerResponse; VerificationHandler::VerificationHandler( - DBConnector& db, TokenManager& tokenManager + TokenManager& tokenManager ) - : m_db(db) - , m_tokenManager(tokenManager) {} + : m_tokenManager(tokenManager) {} void VerificationHandler::handleRequest( HTTPServerRequest& request, HTTPServerResponse& response diff --git a/auth/src/api/handlers/VerificationHandler.hpp b/auth/src/api/handlers/VerificationHandler.hpp index 45b29cc9..6121b844 100644 --- a/auth/src/api/handlers/VerificationHandler.hpp +++ b/auth/src/api/handlers/VerificationHandler.hpp @@ -11,13 +11,12 @@ using Poco::Net::HTTPServerResponse; class VerificationHandler : public HTTPRequestHandler { public: - VerificationHandler(DBConnector& db, TokenManager& tokenManager); + VerificationHandler(TokenManager& tokenManager); public: void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) override; private: - DBConnector& m_db; TokenManager& m_tokenManager; }; From 976342ca2b1c43961ace4672858ec84b5ff09c76 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 06:32:56 +0000 Subject: [PATCH 024/152] Automated formatting --- auth/src/api/AuthRequestHandlerFactory.cpp | 2 +- auth/src/api/handlers/RefreshHandler.cpp | 14 +++++++++----- auth/src/api/handlers/RefreshHandler.hpp | 12 +++++++----- auth/src/api/handlers/VerificationHandler.cpp | 4 +--- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp index f47b3539..93ed50dc 100644 --- a/auth/src/api/AuthRequestHandlerFactory.cpp +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -4,9 +4,9 @@ #include "TokenManager.hpp" #include "handlers/LoginHandler.hpp" #include "handlers/NotFoundHandler.hpp" +#include "handlers/RefreshHandler.hpp" #include "handlers/RegistrationHandler.hpp" #include "handlers/VerificationHandler.hpp" -#include "handlers/RefreshHandler.hpp" #include #include diff --git a/auth/src/api/handlers/RefreshHandler.cpp b/auth/src/api/handlers/RefreshHandler.cpp index 74b51754..47627347 100644 --- a/auth/src/api/handlers/RefreshHandler.cpp +++ b/auth/src/api/handlers/RefreshHandler.cpp @@ -1,4 +1,5 @@ #include "RefreshHandler.hpp" + #include #include #include @@ -6,14 +7,17 @@ #include #include +using Poco::Logger; +using Poco::Net::HTTPResponse; using Poco::Net::HTTPServerRequest; using Poco::Net::HTTPServerResponse; -using Poco::Net::HTTPResponse; -using Poco::Logger; -RefreshHandler::RefreshHandler(TokenManager& tokenManager): m_tokenManager(tokenManager) {} +RefreshHandler::RefreshHandler(TokenManager& tokenManager) + : m_tokenManager(tokenManager) {} -void RefreshHandler::handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) { +void RefreshHandler::handleRequest( + HTTPServerRequest& request, HTTPServerResponse& response +) { Logger& logger = Poco::Util::Application::instance().logger(); logger.information( "POST /api/auth/refresh from %s", request.clientAddress().toString() @@ -53,4 +57,4 @@ void RefreshHandler::handleRequest(HTTPServerRequest& request, HTTPServerRespons e.displayText() ); } -} \ No newline at end of file +} diff --git a/auth/src/api/handlers/RefreshHandler.hpp b/auth/src/api/handlers/RefreshHandler.hpp index 7923291b..f6ba3637 100644 --- a/auth/src/api/handlers/RefreshHandler.hpp +++ b/auth/src/api/handlers/RefreshHandler.hpp @@ -1,18 +1,20 @@ #pragma once -#include #include "../TokenManager.hpp" + +#include using Poco::Net::HTTPRequestHandler; using Poco::Net::HTTPServerRequest; using Poco::Net::HTTPServerResponse; class RefreshHandler : public HTTPRequestHandler { public: - RefreshHandler(TokenManager& tokenManager); + RefreshHandler(TokenManager& tokenManager); public: - void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) override; + void handleRequest(HTTPServerRequest& request, HTTPServerResponse& response) + override; private: - TokenManager& m_tokenManager; -}; \ No newline at end of file + TokenManager& m_tokenManager; +}; diff --git a/auth/src/api/handlers/VerificationHandler.cpp b/auth/src/api/handlers/VerificationHandler.cpp index dcb1babe..4bca03ea 100644 --- a/auth/src/api/handlers/VerificationHandler.cpp +++ b/auth/src/api/handlers/VerificationHandler.cpp @@ -15,9 +15,7 @@ using Poco::Net::HTTPResponse; using Poco::Net::HTTPServerRequest; using Poco::Net::HTTPServerResponse; -VerificationHandler::VerificationHandler( - TokenManager& tokenManager -) +VerificationHandler::VerificationHandler(TokenManager& tokenManager) : m_tokenManager(tokenManager) {} void VerificationHandler::handleRequest( From a18179c6923c73a501250c0bc372bf5314626d50 Mon Sep 17 00:00:00 2001 From: witch2256 Date: Sun, 6 Jul 2025 15:53:27 +0300 Subject: [PATCH 025/152] Login page(not ready yet) --- UI/src/App.jsx | 2 +- UI/src/CSS/auth.css | 119 +++++++++++++++++++++++++++ UI/src/components/pages/auth.jsx | 66 +++++++++++++++ UI/src/components/pages/mainPage.jsx | 2 + 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 UI/src/CSS/auth.css create mode 100644 UI/src/components/pages/auth.jsx diff --git a/UI/src/App.jsx b/UI/src/App.jsx index e9b1ec06..57f66d5b 100644 --- a/UI/src/App.jsx +++ b/UI/src/App.jsx @@ -1,4 +1,4 @@ -import Auth from "./pages/auth.jsx"; +import Auth from "./components/pages/auth.jsx"; import Main from "./components/pages/mainPage.jsx"; import Profile from "./components/pages/profile.jsx"; diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css new file mode 100644 index 00000000..fd60d29c --- /dev/null +++ b/UI/src/CSS/auth.css @@ -0,0 +1,119 @@ +.auth-container{ + display: flex; + text-align: center; + justify-content: center; + align-items: center; + height: 100vh; +} + +.auth-window { + position: fixed; + width: 30rem; + height: 35rem; + background-color: var(--main-1); + border-radius: 0.5rem; + border: var(--main-5) solid 0.05rem; +} + +.auth-window-text{ + margin-top: 1.5rem; + font-size: 1.5rem; +} + +.input-line-container{ + margin-top: 1.5rem; + margin-left: 2.5rem; + height: 20rem; + width: 25rem; + border-top: var(--main-5) solid 0.05rem; +} + +.input-email-text{ + margin-top: 1.5rem; + font-size: 0.9rem; + display: flex; +} + +.input-email-window{ + display: flex; + align-items: center; + justify-content: center; + margin-top: 0.8rem; + height: 2rem; + width: 25rem; + font-size: 1rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; +} + +.input-password-text{ + display: flex; + margin-top: 1.5rem; + font-size: 0.9rem; +} + +.input-password-window{ + display: flex; + align-items: center; + justify-content: center; + margin-top: 0.8rem; + height: 2rem; + width: 25rem; + font-size: 1rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; +} + +.log-in-button{ + margin-top: -3rem; + height: 2rem; + width: 25rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.5rem; + margin-left: 2.5rem; + font-family: Montserrat, serif; + background-color: var(--main-3); + border: var(--main-5) solid 0.05rem; + + &:hover { + transition: 0.1s ease-out; + background-color: var(--main-4); + } +} + +.log-in-button-text{ + display: flex; + justify-content: center; + font-size: 1.2rem; + color: var(--main-0) +} + +.login-button { + position: fixed; + top: 0.5rem; + left: 4.97rem; + width: 4rem; + height: 2rem; + margin-left: 0.4rem; + background-color: var(--main-2); + border: var(--main-5) solid 0.07rem; + border-radius: 0.33rem; + display: flex; + text-align: center; + justify-content: center; + align-items: center; +} + +.login-button:hover { + transition: 0.15s ease-out; + background-color: var(--main-3); +} + +.login-button-text { + display: inline-block; + color: var(--main-0); +} \ No newline at end of file diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx new file mode 100644 index 00000000..c377e563 --- /dev/null +++ b/UI/src/components/pages/auth.jsx @@ -0,0 +1,66 @@ +import React, { useState, useEffect, useRef } from "react"; +import "../../CSS/auth.css"; +import "../../CSS/variables.css" + +const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; + +const Auth = () => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isTouched, setIsTouched] = useState(false); + const emailRef = useRef(null); + + const isValidEmail = EMAIL_REGEXP.test(email); + + const handleEmailChange = (e) => { + setEmail(e.target.value); + }; + + const handlePasswordChange = (e) => { + setPassword(e.target.value); + }; + + useEffect(() => { + if (emailRef.current) { + const showError = isTouched && !EMAIL_REGEXP.test(email) && email !== ""; + emailRef.current.style.borderColor = showError ? "red" : "var(--main-5)"; + } + }, [isValidEmail, isTouched, email]); + + return ( +
+
+
+ Log in +
+
+
+ Enter email: +
+ setIsTouched(true)} + /> + +
+ Enter password: +
+ +
+
+ Log in +
+
+
+ ); +}; + +export default Auth; \ No newline at end of file diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 43c66f84..e4c7b0cd 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -38,6 +38,8 @@ import { updateInputState } from "./mainPage/runnerHandler.jsx"; import { Toaster } from "react-hot-toast"; import { Settings } from "./mainPage/settings.jsx"; import { LOG_LEVELS } from "../codeComponents/logger.jsx"; +import { Link } from "react-router-dom"; + // eslint-disable-next-line react-refresh/only-export-components export const SimulateStateContext = createContext({ From 7e509d442e23631e3d0d2ff7d825a0d9d4f8b31f Mon Sep 17 00:00:00 2001 From: witch2256 Date: Wed, 9 Jul 2025 13:00:04 +0300 Subject: [PATCH 026/152] try to fix colors(not ready) --- UI/src/CSS/auth.css | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index fd60d29c..227abe16 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -4,6 +4,7 @@ justify-content: center; align-items: center; height: 100vh; + background-color: ; } .auth-window { @@ -99,7 +100,7 @@ width: 4rem; height: 2rem; margin-left: 0.4rem; - background-color: var(--main-2); + background-color: var(--menu-lighter); border: var(--main-5) solid 0.07rem; border-radius: 0.33rem; display: flex; @@ -108,11 +109,6 @@ align-items: center; } -.login-button:hover { - transition: 0.15s ease-out; - background-color: var(--main-3); -} - .login-button-text { display: inline-block; color: var(--main-0); From 047c24c8b9a0a490bda101b5783b526777cb3d1d Mon Sep 17 00:00:00 2001 From: witch2256 Date: Wed, 9 Jul 2025 13:15:58 +0300 Subject: [PATCH 027/152] try to fix colors(not ready) --- UI/src/CSS/auth.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index 227abe16..46c80aa9 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -4,7 +4,6 @@ justify-content: center; align-items: center; height: 100vh; - background-color: ; } .auth-window { @@ -107,6 +106,10 @@ text-align: center; justify-content: center; align-items: center; + &:hover { + transition: 0.15s ease-out; + background-color: var(--main-3); + } } .login-button-text { From 15869b16c5a4c0943748a098ce50503372d5e54d Mon Sep 17 00:00:00 2001 From: witch2256 Date: Wed, 9 Jul 2025 14:20:55 +0300 Subject: [PATCH 028/152] here is cringe after merge, i do not know why and try to fix it --- UI/src/components/pages/mainPage.jsx | 460 +++++++++++---------------- 1 file changed, 193 insertions(+), 267 deletions(-) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 549c746f..6d331b87 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -21,31 +21,25 @@ import { useReactFlow, } from "@xyflow/react"; -import CircuitsMenu from "../components/mainPage/circuitsMenu.jsx"; -import Toolbar from "../components/mainPage/toolbar.jsx"; -import ContextMenu from "../components/codeComponents/ContextMenu"; +import CircuitsMenu from "../pages/mainPage/circuitsMenu.jsx"; +import Toolbar from "../pages/mainPage/toolbar.jsx"; -import { initialNodes, nodeTypes } from "../components/codeComponents/nodes"; -import { initialEdges } from "../components/codeComponents/edges"; -import { MinimapSwitch } from "../components/mainPage/switch.jsx"; -import { SelectCanvasBG, SelectTheme } from "../components/mainPage/select.jsx"; +import { initialNodes, nodeTypes } from "../codeComponents/nodes"; +import { initialEdges } from "../codeComponents/edges"; +import { MinimapSwitch } from "./mainPage/switch.jsx"; +import { SelectCanvasBG, SelectTheme } from "./mainPage/select.jsx"; -import { IconSettings, IconMenu } from "../../../assets/ui-icons.jsx"; +import { IconSettings, IconMenu } from "../../../assets/ui-icons"; +import UserIcon from "../../../assets/userIcon.png"; import { Link } from "react-router-dom"; -import { handleSimulateClick } from "../components/mainPage/runnerHandler.jsx"; +import { handleSimulateClick } from "./mainPage/runnerHandler.jsx"; // eslint-disable-next-line react-refresh/only-export-components export const SimulateStateContext = createContext({ simulateState: "idle", setSimulateState: () => {}, - updateInputState: () => {}, -}); - -export const NotificationsLevelContext = createContext({ - logLevel: "idle", - setLogLevel: () => {}, }); // eslint-disable-next-line react-refresh/only-export-components @@ -55,15 +49,7 @@ export function useSimulateState() { throw new Error( "useSimulateState must be used within SimulateStateProvider", ); - return ctx; -} - -export function useNotificationsLevel() { - const ctx = useContext(NotificationsLevelContext); - if (!ctx) - throw new Error( - "useNotificationsLevel must be used within NotificationsLevelProvider", - ); + } return ctx; } @@ -74,16 +60,16 @@ export default function Main() { const [circuitsMenuState, setCircuitsMenuState] = useState(false); const [openSettings, setOpenSettings] = useState(false); const [activeAction, setActiveAction] = useState("cursor"); - const [activeWire, setActiveWire] = useState("step"); + const [activeWire, setActiveWire] = useState("stepWire"); + const [activeButton, setActiveButton] = useState("text"); const [currentBG, setCurrentBG] = useState("dots"); const [showMinimap, setShowMinimap] = useState(true); const [simulateState, setSimulateState] = useState("idle"); const [theme, setTheme] = useState("light"); - const [toastPosition, setToastPosition] = useState("top-center"); - const [logLevel, setLogLevel] = useState(LOG_LEVELS.ERROR); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [wireType, setWireType] = useState("step"); const [menu, setMenu] = useState(null); const ref = useRef(null); @@ -94,15 +80,6 @@ export default function Main() { const socketRef = useRef(null); - const fileInputRef = useRef(null); - - const handleOpenClick = () => { - if (fileInputRef.current) { - fileInputRef.current.click(); - } - }; - - //Load saved settings from localStorage const nodesRef = useRef(nodes); const edgesRef = useRef(edges); @@ -121,7 +98,7 @@ export default function Main() { setNodes(initialNodes); setEdges(initialEdges); } - }, [setEdges, setNodes]); + }, []); useEffect(() => { const circuitData = JSON.stringify({ nodes, edges }); @@ -139,18 +116,19 @@ export default function Main() { useEffect(() => { const saved = localStorage.getItem("userSettings"); if (saved) { - const parsed = JSON.parse(saved); - if (parsed.currentBG) setCurrentBG(parsed.currentBG); - if (typeof parsed.showMinimap === "boolean") - setShowMinimap(parsed.showMinimap); - if (parsed.theme) setTheme(parsed.theme); - if (parsed.activeAction) setActiveAction(parsed.activeAction); - if (parsed.activeWire) setActiveWire(parsed.activeWire); - if (parsed.activeButton) setActiveButton(parsed.activeButton); - if (typeof parsed.openSettings === "boolean") - setOpenSettings(parsed.openSettings); - if (typeof parsed.circuitsMenuState === "boolean") - setCircuitsMenuState(parsed.circuitsMenuState); + const parsed = JSON.parse(saved); + if (parsed.currentBG) setCurrentBG(parsed.currentBG); + if (typeof parsed.showMinimap === "boolean") + setShowMinimap(parsed.showMinimap); + if (parsed.theme) setTheme(parsed.theme); + if (parsed.activeAction) setActiveAction(parsed.activeAction); + if (parsed.activeWire) setActiveWire(parsed.activeWire); + if (parsed.activeButton) setActiveButton(parsed.activeButton); + if (typeof parsed.openSettings === "boolean") + setOpenSettings(parsed.openSettings); + if (typeof parsed.circuitsMenuState === "boolean") + setCircuitsMenuState(parsed.circuitsMenuState); + } }, []); @@ -162,6 +140,7 @@ export default function Main() { theme, activeAction, activeWire, + activeButton, openSettings, circuitsMenuState, }; @@ -172,6 +151,7 @@ export default function Main() { theme, activeAction, activeWire, + activeButton, openSettings, circuitsMenuState, ]); @@ -180,22 +160,22 @@ export default function Main() { useEffect(() => { const handleKeyDown = (e) => { //deleting by clicking delete/backspace(delete for windows and macOS, backspace for windows) - if (e.key === "Delete" || e.key === "Backspace") { + if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); const currentNodes = nodesRef.current; const currentEdges = edgesRef.current; - const selectedNodes = currentNodes.filter((node) => node.selected); - const selectedEdges = currentEdges.filter((edge) => edge.selected); + const selectedNodes = currentNodes.filter(node => node.selected); + const selectedEdges = currentEdges.filter(edge => edge.selected); if (selectedNodes.length === 0 && selectedEdges.length === 0) return; - const nodeIdsToRemove = selectedNodes.map((node) => node.id); + const nodeIdsToRemove = selectedNodes.map(node => node.id); const newNodes = currentNodes.filter( - (node) => !nodeIdsToRemove.includes(node.id), + node => !nodeIdsToRemove.includes(node.id) ); const newEdges = currentEdges.filter( - (edge) => - !selectedEdges.some((selected) => selected.id === edge.id) && - !nodeIdsToRemove.includes(edge.source) && - !nodeIdsToRemove.includes(edge.target), + edge => + !selectedEdges.some(selected => selected.id === edge.id) && + !nodeIdsToRemove.includes(edge.source) && + !nodeIdsToRemove.includes(edge.target) ); setNodes(newNodes); setEdges(newEdges); @@ -216,28 +196,6 @@ export default function Main() { return; } - //Ctrl + Shift + R - Start/stop simulation - if (isCtrlOrCmd && e.shiftKey && e.key.toLowerCase() === "r") { - e.preventDefault(); - handleSimulateClick({ - simulateState, - setSimulateState, - socketRef, - nodes, - edges, - }); - return; - } - - //Ctrl + Shift + O - Load file - if (isCtrlOrCmd && e.key.toLowerCase() === "o") { - e.preventDefault(); - - handleOpenClick(); - - return; - } - //1...6 - Change selected tool const hotkeys = { 1: () => { @@ -248,10 +206,16 @@ export default function Main() { setActiveAction("hand"); setPanOnDrag(true); }, - 3: () => setActiveWire("step"), - 4: () => setActiveWire("straight"), - 5: () => setActiveAction("eraser"), - 6: () => setActiveAction("text"), + 3: () => { + setActiveWire("stepWire"); + setWireType("step"); + }, + 4: () => { + setActiveWire("straightWire"); + setWireType("straight"); + }, + 5: () => setActiveButton("eraser"), + 6: () => setActiveButton("text"), }; if (hotkeys[e.key]) { e.preventDefault(); @@ -262,9 +226,11 @@ export default function Main() { if (e.key === "Escape" && openSettings) { setOpenSettings(false); } + + }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); }, [openSettings]); //Sets current theme to the whole document (наверное) @@ -294,10 +260,10 @@ export default function Main() { }); const newNode = { - id: `${type}_${Date.now()}`, + id: `${type}-${Date.now()}`, type, position, - data: { customId: `${type}_${Date.now()}` }, + data: { customId: `${type}-${Date.now()}` }, }; setNodes((nds) => nds.concat(newNode)); @@ -314,21 +280,6 @@ export default function Main() { setMenu({ id: node.id, name: node.type, - type: "node", - top: event.clientY < pane.height - 200 && event.clientY, - left: event.clientX < pane.width - 200 && event.clientX, - right: event.clientX >= pane.width - 200 && pane.width - event.clientX, - bottom: event.clientY >= pane.height - 200 && pane.height - event.clientY, - }); - }, []); - - const onEdgeContextMenu = useCallback((event, edge) => { - event.preventDefault(); - const pane = ref.current.getBoundingClientRect(); - setMenu({ - id: edge.id, - name: edge.type, - type: "edge", top: event.clientY < pane.height - 200 && event.clientY, left: event.clientX < pane.width - 200 && event.clientX, right: event.clientX >= pane.width - 200 && pane.width - event.clientX, @@ -388,14 +339,10 @@ export default function Main() { }); const newNode = { - id: `${type}_${Date.now()}`, + id: `${type}-${Date.now()}`, type, position, - data: { - customId: `${type}_${Date.now()}`, - simState: simulateState, - value: false, - }, + data: { customId: `${type}-${Date.now()}`, simState: simulateState }, }; setNodes((nds) => nds.concat(newNode)); @@ -442,7 +389,7 @@ export default function Main() { if (distance < minDistance) { minDistance = distance; closestEdge = { - id: `temp_${internalNode.id}_${srcHandle.id}_to_${node.id}_${tgtHandle.id}`, + id: `temp-${internalNode.id}-${srcHandle.id}-to-${node.id}-${tgtHandle.id}`, source: internalNode.id, sourceHandle: srcHandle.id, target: node.id, @@ -477,7 +424,7 @@ export default function Main() { if (distance < minDistance) { minDistance = distance; closestEdge = { - id: `temp_${node.id}_${srcHandle.id}_to_${internalNode.id}_${tgtHandle.id}`, + id: `temp-${node.id}-${srcHandle.id}-to-${internalNode.id}-${tgtHandle.id}`, source: node.id, sourceHandle: srcHandle.id, target: internalNode.id, @@ -538,106 +485,66 @@ export default function Main() { : BackgroundVariant.Lines; return ( - - - <> - e.preventDefault()} - onInit={setReactFlowInstance} - nodeTypes={nodeTypes} - panOnDrag={panOnDrag} - selectionOnDrag - panOnScroll - snapToGrid - snapGrid={[GAP_SIZE, GAP_SIZE]} - selectionMode={SelectionMode.Partial} - minZoom={0.2} - maxZoom={10} - > - - + <> + e.preventDefault()} + onInit={setReactFlowInstance} + nodeTypes={nodeTypes} + panOnDrag={panOnDrag} + selectionOnDrag + panOnScroll + snapToGrid + snapGrid={[GAP_SIZE, GAP_SIZE]} + selectionMode={SelectionMode.Partial} + minZoom={0.2} + maxZoom={10} + > + + + {showMinimap && ( + - {showMinimap && ( - - )} - - - {menu && menu.type === "node" && ( - - )} - - {menu && menu.type === "edge" && ( - )} + {menu && } + - setCircuitsMenuState(!circuitsMenuState)} + > + - - + + Log in -
{ - setMenu(null); - setOpenSettings(false); - }} - /> - { - setMenu(null); - setOpenSettings(false); - }} - /> - - - - - handleSimulateClick({ - simulateState, - setSimulateState, - socketRef, - nodes, - edges, - }) - } +
setOpenSettings(false)} + /> +
+

Settings

+ + User + UserName + +
+

Show mini-map

+ +
+
+

Canvas background

+ +
+
+

Theme

+ +
+ + - - - +
+ + + + + handleSimulateClick({ + simulateState, + setSimulateState, + socketRef, + nodes, + edges, + }) + } + /> + + ); } From 2cfd018d113a388665d6650cf528f8e8f93f2826 Mon Sep 17 00:00:00 2001 From: witch2256 Date: Wed, 9 Jul 2025 14:44:37 +0300 Subject: [PATCH 029/152] all fixed and working correct now --- UI/src/components/pages/mainPage.jsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 4fb92210..3c4cbdd5 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -38,6 +38,8 @@ import { updateInputState } from "./mainPage/runnerHandler.jsx"; import { Toaster } from "react-hot-toast"; import { Settings } from "./mainPage/settings.jsx"; import { LOG_LEVELS } from "../codeComponents/logger.jsx"; +import UserIcon from "../../../assets/userIcon.png"; +import {Link} from "react-router-dom"; // eslint-disable-next-line react-refresh/only-export-components export const SimulateStateContext = createContext({ @@ -906,6 +908,14 @@ export default function Main() { /> + + Log in + +
{ From c9bf1d4673549e9c4b3ea9f56338c887903f826d Mon Sep 17 00:00:00 2001 From: arsenez Date: Wed, 9 Jul 2025 21:31:55 +0300 Subject: [PATCH 030/152] Add temporary solution for blacklisting tokens --- auth/src/api/TokenManager.cpp | 3 ++- auth/src/api/TokenManager.hpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/auth/src/api/TokenManager.cpp b/auth/src/api/TokenManager.cpp index 99ac14c5..b07c3d4e 100644 --- a/auth/src/api/TokenManager.cpp +++ b/auth/src/api/TokenManager.cpp @@ -73,6 +73,7 @@ User TokenManager::getUser(std::string const& token) const { } std::string TokenManager::refresh(std::string const& token) { + m_blacklist.insert(token); return generate(getUser(token)); } @@ -87,7 +88,7 @@ bool TokenManager::_verify( switch (realType) { case ACCESS: return !out.getIssuedAt().isElapsed(15LL * 60000000LL); case REFRESH: - return !out.getIssuedAt().isElapsed(30LL * 24LL * 60LL * 60000000LL); + return !out.getIssuedAt().isElapsed(30LL * 24LL * 60LL * 60000000LL) && (m_blacklist.find(token) == m_blacklist.cend()); default: return false; } } else { diff --git a/auth/src/api/TokenManager.hpp b/auth/src/api/TokenManager.hpp index 05a50ef4..c9ba59e9 100644 --- a/auth/src/api/TokenManager.hpp +++ b/auth/src/api/TokenManager.hpp @@ -4,6 +4,7 @@ #include #include +#include #include using Poco::JWT::Signer; @@ -32,4 +33,5 @@ class TokenManager { private: Signer m_signer; + std::unordered_set m_blacklist; }; From e31d6705613785584cb5fb5fd3d709452879ae66 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:32:17 +0000 Subject: [PATCH 031/152] Automated formatting --- auth/src/api/TokenManager.cpp | 3 ++- auth/src/api/TokenManager.hpp | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/auth/src/api/TokenManager.cpp b/auth/src/api/TokenManager.cpp index b07c3d4e..c4cde77e 100644 --- a/auth/src/api/TokenManager.cpp +++ b/auth/src/api/TokenManager.cpp @@ -88,7 +88,8 @@ bool TokenManager::_verify( switch (realType) { case ACCESS: return !out.getIssuedAt().isElapsed(15LL * 60000000LL); case REFRESH: - return !out.getIssuedAt().isElapsed(30LL * 24LL * 60LL * 60000000LL) && (m_blacklist.find(token) == m_blacklist.cend()); + return !out.getIssuedAt().isElapsed(30LL * 24LL * 60LL * 60000000LL) && + (m_blacklist.find(token) == m_blacklist.cend()); default: return false; } } else { diff --git a/auth/src/api/TokenManager.hpp b/auth/src/api/TokenManager.hpp index c9ba59e9..2df4fa15 100644 --- a/auth/src/api/TokenManager.hpp +++ b/auth/src/api/TokenManager.hpp @@ -4,8 +4,8 @@ #include #include -#include #include +#include using Poco::JWT::Signer; @@ -32,6 +32,6 @@ class TokenManager { const; private: - Signer m_signer; - std::unordered_set m_blacklist; + Signer m_signer; + std::unordered_set< std::string > m_blacklist; }; From 4a7695ca40d5eefba2aba233833b381f24c32639 Mon Sep 17 00:00:00 2001 From: arsenez Date: Thu, 10 Jul 2025 00:47:34 +0300 Subject: [PATCH 032/152] Create Dockerfile for auth microservice --- auth/Dockerfile | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 auth/Dockerfile diff --git a/auth/Dockerfile b/auth/Dockerfile new file mode 100644 index 00000000..3c59d63a --- /dev/null +++ b/auth/Dockerfile @@ -0,0 +1,20 @@ +FROM nixos/nix:latest AS build + +WORKDIR /app +COPY CMakeLists.txt . +COPY src/ src +COPY default.nix . + +RUN --mount=type=cache,target=/tmp/nix-store cp -R /tmp/nix-store/nix /nix +RUN nix-build +RUN --mount=type=cache,target=/tmp/nix-store cp -R /nix /tmp/nix-store/nix +RUN mkdir nix-deps +RUN cp -R $(nix-store -qR result/) nix-deps + +FROM scratch + +WORKDIR /app + +COPY --from=build /app/nix-deps /nix/store +COPY --from=build /app/result /app +ENTRYPOINT [ "/app/bin/VCDAuth" ] \ No newline at end of file From b58568262c90cf7497ede5235dfd255adfd026dc Mon Sep 17 00:00:00 2001 From: witch2256 Date: Fri, 11 Jul 2025 14:07:47 +0300 Subject: [PATCH 033/152] Log in page modification --- UI/src/CSS/auth.css | 25 +++++++++++++++++++++++++ UI/src/CSS/profile.css | 1 + UI/src/components/pages/auth.jsx | 19 ++++++++++++++++--- UI/src/components/pages/mainPage.jsx | 1 - 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index 46c80aa9..7bf2c76e 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -4,6 +4,7 @@ justify-content: center; align-items: center; height: 100vh; + background-color: var(--main-2); } .auth-window { @@ -13,11 +14,13 @@ background-color: var(--main-1); border-radius: 0.5rem; border: var(--main-5) solid 0.05rem; + } .auth-window-text{ margin-top: 1.5rem; font-size: 1.5rem; + color: var(--main-0) } .input-line-container{ @@ -32,6 +35,7 @@ margin-top: 1.5rem; font-size: 0.9rem; display: flex; + color: var(--main-0) } .input-email-window{ @@ -51,6 +55,7 @@ display: flex; margin-top: 1.5rem; font-size: 0.9rem; + color: var(--main-0) } .input-password-window{ @@ -92,6 +97,26 @@ color: var(--main-0) } +.register-text{ + font-size: 1.1rem; + font-family: Montserrat, serif; + margin-top: 5rem; +} + +.register-link{ + display: flex; + font-size: 0.2rem; + font-family: Montserrat, serif; + margin-left: 12.25rem; +} + +.register-link-text{ + margin-left: 0.5rem; + font-size: 1rem; + font-family: Montserrat, serif; + color: var(--main-0) +} + .login-button { position: fixed; top: 0.5rem; diff --git a/UI/src/CSS/profile.css b/UI/src/CSS/profile.css index d64efc2a..d88a5d49 100644 --- a/UI/src/CSS/profile.css +++ b/UI/src/CSS/profile.css @@ -2,6 +2,7 @@ display: flex; flex-direction: column; height: 100vh; + background-color: var(--main-2); } .header2 { diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index c377e563..9a6aff28 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useRef } from "react"; import "../../CSS/auth.css"; import "../../CSS/variables.css" +import {Link} from "react-router-dom"; const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; @@ -29,6 +30,7 @@ const Auth = () => { return (
+
Log in @@ -55,9 +57,20 @@ const Auth = () => { onChange={handlePasswordChange} />
-
- Log in -
+ + Log in + +
Have no account?
+ + Register +
); diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 3c4cbdd5..562e967e 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -38,7 +38,6 @@ import { updateInputState } from "./mainPage/runnerHandler.jsx"; import { Toaster } from "react-hot-toast"; import { Settings } from "./mainPage/settings.jsx"; import { LOG_LEVELS } from "../codeComponents/logger.jsx"; -import UserIcon from "../../../assets/userIcon.png"; import {Link} from "react-router-dom"; // eslint-disable-next-line react-refresh/only-export-components From cc9cf81a2f8e96326da3453f2062c8dd96cba511 Mon Sep 17 00:00:00 2001 From: witch2256 Date: Fri, 11 Jul 2025 15:03:26 +0300 Subject: [PATCH 034/152] register page added --- UI/src/App.jsx | 2 + UI/src/CSS/reg.css | 118 +++++++++++++++++++++++ UI/src/components/pages/register.jsx | 138 +++++++++++++++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 UI/src/CSS/reg.css create mode 100644 UI/src/components/pages/register.jsx diff --git a/UI/src/App.jsx b/UI/src/App.jsx index ee8b845b..bc9bb538 100644 --- a/UI/src/App.jsx +++ b/UI/src/App.jsx @@ -2,6 +2,7 @@ import Auth from "./components/pages/auth.jsx"; import Main from "./components/pages/mainPage.jsx"; import Profile from "./components/pages/profile.jsx"; import HelloPage from "./components/pages/hello-page.jsx"; +import Registration from "./components/pages/register.jsx" import "@xyflow/react/dist/style.css"; @@ -41,6 +42,7 @@ function App() { } /> } /> } /> + } />
diff --git a/UI/src/CSS/reg.css b/UI/src/CSS/reg.css new file mode 100644 index 00000000..d0356ef4 --- /dev/null +++ b/UI/src/CSS/reg.css @@ -0,0 +1,118 @@ +.reg-container{ + display: flex; + text-align: center; + justify-content: center; + align-items: center; + height: 100vh; + background-color: var(--main-2); +} + +.reg-window { + position: fixed; + width: 30rem; + height: 40rem; + background-color: var(--main-1); + border-radius: 0.5rem; + border: var(--main-5) solid 0.05rem; +} + +.reg-window-text{ + margin-top: 1.5rem; + font-size: 1.5rem; + color: var(--main-0) +} + +.input-username-text{ + margin-top: 1.5rem; + font-size: 0.9rem; + display: flex; + color: var(--main-0) +} + +.input-username-window{ + margin-top: 0.8rem; + height: 2rem; + width: 25rem; + font-size: 1rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; +} + +.input-line-container{ + margin-top: 1.5rem; + margin-left: 2.5rem; + height: 20rem; + width: 25rem; + border-top: var(--main-5) solid 0.05rem; +} + +.input-email-text{ + margin-top: 1.5rem; + font-size: 0.9rem; + display: flex; + color: var(--main-0) +} + +.input-email-window{ + margin-top: 0.8rem; + height: 2rem; + width: 25rem; + font-size: 1rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; +} + +.input-password-text{ + display: flex; + margin-top: 1.5rem; + font-size: 0.9rem; + color: var(--main-0); +} + +.input-password-window{ + display: flex; + align-items: center; + justify-content: center; + margin-top: 0.8rem; + height: 2rem; + width: 25rem; + font-size: 1rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; +} + +.input-confirm-password-text{ + display: flex; + margin-top: 1.5rem; + font-size: 0.9rem; + color: var(--main-0); +} + +.reg-button{ + margin-top: 8rem; + height: 2rem; + width: 25rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.5rem; + margin-left: 2.5rem; + font-family: Montserrat, serif; + background-color: var(--main-3); + border: var(--main-5) solid 0.05rem; + + &:hover { + transition: 0.1s ease-out; + background-color: var(--main-4); + } +} + +.reg-button-text{ + display: flex; + justify-content: center; + font-size: 1.2rem; + color: var(--main-0) +} \ No newline at end of file diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx new file mode 100644 index 00000000..400bf207 --- /dev/null +++ b/UI/src/components/pages/register.jsx @@ -0,0 +1,138 @@ +import React, { useState, useEffect, useRef } from "react"; +import "../../CSS/reg.css"; +import "../../CSS/variables.css" +import {Link} from "react-router-dom"; + +const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; +const USERNAME_REGEXP = /^[a-zA-Z0-9._-]{4,25}$/; + +const Auth = () => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isTouched, setIsTouched] = useState(false); + const emailRef = useRef(null); + const [username, setUsername] = useState(""); + const [isUsernameTouched, setIsUsernameTouched] = useState(false); + const usernameRef = useRef(null); + + const [confirmPassword, setConfirmPassword] = useState(""); + const [isConfirmPasswordTouched, setIsConfirmPasswordTouched] = useState(false); + const passwordRef = useRef(null); + const confirmPasswordRef = useRef(null); + + const isValidUsername = USERNAME_REGEXP.test(username); + + const isValidEmail = EMAIL_REGEXP.test(email); + + const handleUsernameChange = (e) => { + setUsername(e.target.value); + }; + + const handleEmailChange = (e) => { + setEmail(e.target.value); + }; + + const handlePasswordChange = (e) => { + setPassword(e.target.value); + }; + + const handleConfirmPasswordChange = (e) => { + setConfirmPassword(e.target.value); + }; + + const passwordsMatch = password === confirmPassword; + + useEffect(() => { + if (usernameRef.current) { + const showError = isUsernameTouched && !isValidUsername && username !== ""; + usernameRef.current.style.borderColor = showError + ? "red" + : "var(--main-5)"; + } + + if (emailRef.current) { + const showEmailError = isTouched && !isValidEmail && email !== ""; + emailRef.current.style.borderColor = showEmailError + ? "red" + : "var(--main-5)"; + } + + if (passwordRef.current) { + passwordRef.current.style.borderColor = "var(--main-5)"; + } + + if (confirmPasswordRef.current) { + const showError = isConfirmPasswordTouched && !passwordsMatch && confirmPassword !== ""; + confirmPasswordRef.current.style.borderColor = showError ? "red" : "var(--main-5)"; + } + }, [ + isValidUsername, isUsernameTouched, username, isValidEmail, + isTouched, email, passwordsMatch, isConfirmPasswordTouched, + confirmPassword + ]); + + return ( +
+
+
+ Registration +
+
+
+ Enter username: +
+ setIsUsernameTouched(true)} + /> + +
+ Enter email: +
+ setIsTouched(true)} + /> + +
+ Enter password: +
+ +
Confirm password:
+ setIsConfirmPasswordTouched(true)} + /> +
+ + Register + +
+
+ ); +}; + +export default Auth; \ No newline at end of file From faa696204bf5abf2ea4be6404900237ee7613e0a Mon Sep 17 00:00:00 2001 From: witch2256 Date: Fri, 11 Jul 2025 18:47:55 +0300 Subject: [PATCH 035/152] changes in color of input window and its text --- UI/src/CSS/auth.css | 2 +- UI/src/CSS/reg.css | 26 ++++++++++++++++---------- UI/src/components/pages/register.jsx | 6 +++--- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index 7bf2c76e..33a12d74 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -14,7 +14,6 @@ background-color: var(--main-1); border-radius: 0.5rem; border: var(--main-5) solid 0.05rem; - } .auth-window-text{ @@ -101,6 +100,7 @@ font-size: 1.1rem; font-family: Montserrat, serif; margin-top: 5rem; + color: var(--main-0) } .register-link{ diff --git a/UI/src/CSS/reg.css b/UI/src/CSS/reg.css index d0356ef4..8584cd59 100644 --- a/UI/src/CSS/reg.css +++ b/UI/src/CSS/reg.css @@ -24,19 +24,21 @@ .input-username-text{ margin-top: 1.5rem; - font-size: 0.9rem; + font-size: 1rem; display: flex; - color: var(--main-0) + color: var(--main-0); } .input-username-window{ - margin-top: 0.8rem; + margin-top: 0.7rem; height: 2rem; width: 25rem; - font-size: 1rem; + font-size: 0.9rem; border: var(--main-5) solid 0.05rem; border-radius: 0.5rem; font-family: Montserrat, serif; + background-color: var(--main-2); + color: var(--main-0); } .input-line-container{ @@ -49,25 +51,27 @@ .input-email-text{ margin-top: 1.5rem; - font-size: 0.9rem; + font-size: 1rem; display: flex; color: var(--main-0) } .input-email-window{ - margin-top: 0.8rem; + margin-top: 0.7rem; height: 2rem; width: 25rem; - font-size: 1rem; + font-size: 0.9rem; border: var(--main-5) solid 0.05rem; border-radius: 0.5rem; font-family: Montserrat, serif; + background-color: var(--main-2); + color: var(--main-0); } .input-password-text{ display: flex; margin-top: 1.5rem; - font-size: 0.9rem; + font-size: 1rem; color: var(--main-0); } @@ -75,19 +79,21 @@ display: flex; align-items: center; justify-content: center; - margin-top: 0.8rem; + margin-top: 0.7rem; height: 2rem; width: 25rem; font-size: 1rem; border: var(--main-5) solid 0.05rem; border-radius: 0.5rem; font-family: Montserrat, serif; + background-color: var(--main-2); + color: var(--main-0); } .input-confirm-password-text{ display: flex; margin-top: 1.5rem; - font-size: 0.9rem; + font-size: 1rem; color: var(--main-0); } diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index 400bf207..84ccadd6 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -79,7 +79,7 @@ const Auth = () => {
- Enter username: + Username
{ />
- Enter email: + Email
{ />
- Enter password: + Password
Date: Fri, 11 Jul 2025 19:15:55 +0300 Subject: [PATCH 036/152] placeholder added --- UI/src/components/pages/auth.jsx | 1 + UI/src/components/pages/register.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index 9a6aff28..ce932285 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -45,6 +45,7 @@ const Auth = () => { value={email} onChange={handleEmailChange} onBlur={() => setIsTouched(true)} + placeholder="myEmail@example.com" />
diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index 84ccadd6..35b9a656 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -100,6 +100,7 @@ const Auth = () => { value={email} onChange={handleEmailChange} onBlur={() => setIsTouched(true)} + placeholder="myEmail@example.com" />
From c83f56ccdbcee3bd8455d03bac0b183b843e0d1f Mon Sep 17 00:00:00 2001 From: witch2256 Date: Fri, 11 Jul 2025 20:29:07 +0300 Subject: [PATCH 037/152] merge with dev and fixed button issue --- UI/src/CSS/auth.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index 33a12d74..71a63803 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -119,7 +119,7 @@ .login-button { position: fixed; - top: 0.5rem; + top: 3.05rem; left: 4.97rem; width: 4rem; height: 2rem; From fa1282f6478883b67a14c2a9fbdd0ca23d7b96c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:29:43 +0000 Subject: [PATCH 038/152] Automated formatting --- UI/src/App.jsx | 2 +- UI/src/CSS/auth.css | 222 +++++++++++++-------------- UI/src/CSS/reg.css | 196 +++++++++++------------ UI/src/components/pages/auth.jsx | 27 ++-- UI/src/components/pages/mainPage.jsx | 2 +- UI/src/components/pages/register.jsx | 60 +++++--- 6 files changed, 255 insertions(+), 254 deletions(-) diff --git a/UI/src/App.jsx b/UI/src/App.jsx index ccbe10e1..58a00dc7 100644 --- a/UI/src/App.jsx +++ b/UI/src/App.jsx @@ -2,7 +2,7 @@ import Auth from "./components/pages/auth.jsx"; import Main from "./components/pages/mainPage.jsx"; import Profile from "./components/pages/profile.jsx"; import HelloPage from "./components/pages/hello-page.jsx"; -import Registration from "./components/pages/register.jsx" +import Registration from "./components/pages/register.jsx"; import "@xyflow/react/dist/style.css"; diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index 71a63803..f6ee42fc 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -1,143 +1,143 @@ -.auth-container{ - display: flex; - text-align: center; - justify-content: center; - align-items: center; - height: 100vh; - background-color: var(--main-2); +.auth-container { + display: flex; + text-align: center; + justify-content: center; + align-items: center; + height: 100vh; + background-color: var(--main-2); } .auth-window { - position: fixed; - width: 30rem; - height: 35rem; - background-color: var(--main-1); - border-radius: 0.5rem; - border: var(--main-5) solid 0.05rem; + position: fixed; + width: 30rem; + height: 35rem; + background-color: var(--main-1); + border-radius: 0.5rem; + border: var(--main-5) solid 0.05rem; } -.auth-window-text{ - margin-top: 1.5rem; - font-size: 1.5rem; - color: var(--main-0) +.auth-window-text { + margin-top: 1.5rem; + font-size: 1.5rem; + color: var(--main-0); } -.input-line-container{ - margin-top: 1.5rem; - margin-left: 2.5rem; - height: 20rem; - width: 25rem; - border-top: var(--main-5) solid 0.05rem; +.input-line-container { + margin-top: 1.5rem; + margin-left: 2.5rem; + height: 20rem; + width: 25rem; + border-top: var(--main-5) solid 0.05rem; } -.input-email-text{ - margin-top: 1.5rem; - font-size: 0.9rem; - display: flex; - color: var(--main-0) +.input-email-text { + margin-top: 1.5rem; + font-size: 0.9rem; + display: flex; + color: var(--main-0); } -.input-email-window{ - display: flex; - align-items: center; - justify-content: center; - margin-top: 0.8rem; - height: 2rem; - width: 25rem; - font-size: 1rem; - border: var(--main-5) solid 0.05rem; - border-radius: 0.5rem; - font-family: Montserrat, serif; +.input-email-window { + display: flex; + align-items: center; + justify-content: center; + margin-top: 0.8rem; + height: 2rem; + width: 25rem; + font-size: 1rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; } -.input-password-text{ - display: flex; - margin-top: 1.5rem; - font-size: 0.9rem; - color: var(--main-0) +.input-password-text { + display: flex; + margin-top: 1.5rem; + font-size: 0.9rem; + color: var(--main-0); } -.input-password-window{ - display: flex; - align-items: center; - justify-content: center; - margin-top: 0.8rem; - height: 2rem; - width: 25rem; - font-size: 1rem; - border: var(--main-5) solid 0.05rem; - border-radius: 0.5rem; - font-family: Montserrat, serif; +.input-password-window { + display: flex; + align-items: center; + justify-content: center; + margin-top: 0.8rem; + height: 2rem; + width: 25rem; + font-size: 1rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; } -.log-in-button{ - margin-top: -3rem; - height: 2rem; - width: 25rem; - display: flex; - align-items: center; - justify-content: center; - border-radius: 0.5rem; - margin-left: 2.5rem; - font-family: Montserrat, serif; - background-color: var(--main-3); - border: var(--main-5) solid 0.05rem; +.log-in-button { + margin-top: -3rem; + height: 2rem; + width: 25rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.5rem; + margin-left: 2.5rem; + font-family: Montserrat, serif; + background-color: var(--main-3); + border: var(--main-5) solid 0.05rem; - &:hover { - transition: 0.1s ease-out; - background-color: var(--main-4); - } + &:hover { + transition: 0.1s ease-out; + background-color: var(--main-4); + } } -.log-in-button-text{ - display: flex; - justify-content: center; - font-size: 1.2rem; - color: var(--main-0) +.log-in-button-text { + display: flex; + justify-content: center; + font-size: 1.2rem; + color: var(--main-0); } -.register-text{ - font-size: 1.1rem; - font-family: Montserrat, serif; - margin-top: 5rem; - color: var(--main-0) +.register-text { + font-size: 1.1rem; + font-family: Montserrat, serif; + margin-top: 5rem; + color: var(--main-0); } -.register-link{ - display: flex; - font-size: 0.2rem; - font-family: Montserrat, serif; - margin-left: 12.25rem; +.register-link { + display: flex; + font-size: 0.2rem; + font-family: Montserrat, serif; + margin-left: 12.25rem; } -.register-link-text{ - margin-left: 0.5rem; - font-size: 1rem; - font-family: Montserrat, serif; - color: var(--main-0) +.register-link-text { + margin-left: 0.5rem; + font-size: 1rem; + font-family: Montserrat, serif; + color: var(--main-0); } .login-button { - position: fixed; - top: 3.05rem; - left: 4.97rem; - width: 4rem; - height: 2rem; - margin-left: 0.4rem; - background-color: var(--menu-lighter); - border: var(--main-5) solid 0.07rem; - border-radius: 0.33rem; - display: flex; - text-align: center; - justify-content: center; - align-items: center; - &:hover { - transition: 0.15s ease-out; - background-color: var(--main-3); - } + position: fixed; + top: 3.05rem; + left: 4.97rem; + width: 4rem; + height: 2rem; + margin-left: 0.4rem; + background-color: var(--menu-lighter); + border: var(--main-5) solid 0.07rem; + border-radius: 0.33rem; + display: flex; + text-align: center; + justify-content: center; + align-items: center; + &:hover { + transition: 0.15s ease-out; + background-color: var(--main-3); + } } .login-button-text { - display: inline-block; - color: var(--main-0); -} \ No newline at end of file + display: inline-block; + color: var(--main-0); +} diff --git a/UI/src/CSS/reg.css b/UI/src/CSS/reg.css index 8584cd59..c8e2f724 100644 --- a/UI/src/CSS/reg.css +++ b/UI/src/CSS/reg.css @@ -1,124 +1,124 @@ -.reg-container{ - display: flex; - text-align: center; - justify-content: center; - align-items: center; - height: 100vh; - background-color: var(--main-2); +.reg-container { + display: flex; + text-align: center; + justify-content: center; + align-items: center; + height: 100vh; + background-color: var(--main-2); } .reg-window { - position: fixed; - width: 30rem; - height: 40rem; - background-color: var(--main-1); - border-radius: 0.5rem; - border: var(--main-5) solid 0.05rem; + position: fixed; + width: 30rem; + height: 40rem; + background-color: var(--main-1); + border-radius: 0.5rem; + border: var(--main-5) solid 0.05rem; } -.reg-window-text{ - margin-top: 1.5rem; - font-size: 1.5rem; - color: var(--main-0) +.reg-window-text { + margin-top: 1.5rem; + font-size: 1.5rem; + color: var(--main-0); } -.input-username-text{ - margin-top: 1.5rem; - font-size: 1rem; - display: flex; - color: var(--main-0); +.input-username-text { + margin-top: 1.5rem; + font-size: 1rem; + display: flex; + color: var(--main-0); } -.input-username-window{ - margin-top: 0.7rem; - height: 2rem; - width: 25rem; - font-size: 0.9rem; - border: var(--main-5) solid 0.05rem; - border-radius: 0.5rem; - font-family: Montserrat, serif; - background-color: var(--main-2); - color: var(--main-0); +.input-username-window { + margin-top: 0.7rem; + height: 2rem; + width: 25rem; + font-size: 0.9rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; + background-color: var(--main-2); + color: var(--main-0); } -.input-line-container{ - margin-top: 1.5rem; - margin-left: 2.5rem; - height: 20rem; - width: 25rem; - border-top: var(--main-5) solid 0.05rem; +.input-line-container { + margin-top: 1.5rem; + margin-left: 2.5rem; + height: 20rem; + width: 25rem; + border-top: var(--main-5) solid 0.05rem; } -.input-email-text{ - margin-top: 1.5rem; - font-size: 1rem; - display: flex; - color: var(--main-0) +.input-email-text { + margin-top: 1.5rem; + font-size: 1rem; + display: flex; + color: var(--main-0); } -.input-email-window{ - margin-top: 0.7rem; - height: 2rem; - width: 25rem; - font-size: 0.9rem; - border: var(--main-5) solid 0.05rem; - border-radius: 0.5rem; - font-family: Montserrat, serif; - background-color: var(--main-2); - color: var(--main-0); +.input-email-window { + margin-top: 0.7rem; + height: 2rem; + width: 25rem; + font-size: 0.9rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; + background-color: var(--main-2); + color: var(--main-0); } -.input-password-text{ - display: flex; - margin-top: 1.5rem; - font-size: 1rem; - color: var(--main-0); +.input-password-text { + display: flex; + margin-top: 1.5rem; + font-size: 1rem; + color: var(--main-0); } -.input-password-window{ - display: flex; - align-items: center; - justify-content: center; - margin-top: 0.7rem; - height: 2rem; - width: 25rem; - font-size: 1rem; - border: var(--main-5) solid 0.05rem; - border-radius: 0.5rem; - font-family: Montserrat, serif; - background-color: var(--main-2); - color: var(--main-0); +.input-password-window { + display: flex; + align-items: center; + justify-content: center; + margin-top: 0.7rem; + height: 2rem; + width: 25rem; + font-size: 1rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; + background-color: var(--main-2); + color: var(--main-0); } -.input-confirm-password-text{ - display: flex; - margin-top: 1.5rem; - font-size: 1rem; - color: var(--main-0); +.input-confirm-password-text { + display: flex; + margin-top: 1.5rem; + font-size: 1rem; + color: var(--main-0); } -.reg-button{ - margin-top: 8rem; - height: 2rem; - width: 25rem; - display: flex; - align-items: center; - justify-content: center; - border-radius: 0.5rem; - margin-left: 2.5rem; - font-family: Montserrat, serif; - background-color: var(--main-3); - border: var(--main-5) solid 0.05rem; +.reg-button { + margin-top: 8rem; + height: 2rem; + width: 25rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.5rem; + margin-left: 2.5rem; + font-family: Montserrat, serif; + background-color: var(--main-3); + border: var(--main-5) solid 0.05rem; - &:hover { - transition: 0.1s ease-out; - background-color: var(--main-4); - } + &:hover { + transition: 0.1s ease-out; + background-color: var(--main-4); + } } -.reg-button-text{ - display: flex; - justify-content: center; - font-size: 1.2rem; - color: var(--main-0) -} \ No newline at end of file +.reg-button-text { + display: flex; + justify-content: center; + font-size: 1.2rem; + color: var(--main-0); +} diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index ce932285..dabbc6af 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -1,9 +1,10 @@ import React, { useState, useEffect, useRef } from "react"; import "../../CSS/auth.css"; -import "../../CSS/variables.css" -import {Link} from "react-router-dom"; +import "../../CSS/variables.css"; +import { Link } from "react-router-dom"; -const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; +const EMAIL_REGEXP = + /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; const Auth = () => { const [email, setEmail] = useState(""); @@ -30,15 +31,10 @@ const Auth = () => { return (
-
-
- Log in -
+
Log in
-
- Enter email: -
+
Enter email:
{ placeholder="myEmail@example.com" /> -
- Enter password: -
+
Enter password:
{ Log in
Have no account?
- + Register
@@ -77,4 +68,4 @@ const Auth = () => { ); }; -export default Auth; \ No newline at end of file +export default Auth; diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index aaeba276..3a8ee623 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -38,7 +38,7 @@ import { handleSimulateClick } from "./mainPage/runnerHandler.jsx"; import { updateInputState } from "./mainPage/runnerHandler.jsx"; import { LOG_LEVELS } from "../codeComponents/logger.jsx"; import { nanoid } from "nanoid"; -import {Link} from "react-router-dom"; +import { Link } from "react-router-dom"; import { copyElements as copyElementsUtil } from "../utils/copyElements.js"; import { cutElements as cutElementsUtil } from "../utils/cutElements.js"; diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index 35b9a656..ccc8be77 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -1,9 +1,10 @@ import React, { useState, useEffect, useRef } from "react"; import "../../CSS/reg.css"; -import "../../CSS/variables.css" -import {Link} from "react-router-dom"; +import "../../CSS/variables.css"; +import { Link } from "react-router-dom"; -const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; +const EMAIL_REGEXP = + /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; const USERNAME_REGEXP = /^[a-zA-Z0-9._-]{4,25}$/; const Auth = () => { @@ -16,7 +17,8 @@ const Auth = () => { const usernameRef = useRef(null); const [confirmPassword, setConfirmPassword] = useState(""); - const [isConfirmPasswordTouched, setIsConfirmPasswordTouched] = useState(false); + const [isConfirmPasswordTouched, setIsConfirmPasswordTouched] = + useState(false); const passwordRef = useRef(null); const confirmPasswordRef = useRef(null); @@ -44,7 +46,8 @@ const Auth = () => { useEffect(() => { if (usernameRef.current) { - const showError = isUsernameTouched && !isValidUsername && username !== ""; + const showError = + isUsernameTouched && !isValidUsername && username !== ""; usernameRef.current.style.borderColor = showError ? "red" : "var(--main-5)"; @@ -62,38 +65,43 @@ const Auth = () => { } if (confirmPasswordRef.current) { - const showError = isConfirmPasswordTouched && !passwordsMatch && confirmPassword !== ""; - confirmPasswordRef.current.style.borderColor = showError ? "red" : "var(--main-5)"; + const showError = + isConfirmPasswordTouched && !passwordsMatch && confirmPassword !== ""; + confirmPasswordRef.current.style.borderColor = showError + ? "red" + : "var(--main-5)"; } }, [ - isValidUsername, isUsernameTouched, username, isValidEmail, - isTouched, email, passwordsMatch, isConfirmPasswordTouched, - confirmPassword + isValidUsername, + isUsernameTouched, + username, + isValidEmail, + isTouched, + email, + passwordsMatch, + isConfirmPasswordTouched, + confirmPassword, ]); return (
-
- Registration -
+
Registration
-
- Username -
+
Username
setIsUsernameTouched(true)} /> -
- Email -
+
Email
{ placeholder="myEmail@example.com" /> -
- Password -
+
Password
{ { ); }; -export default Auth; \ No newline at end of file +export default Auth; From 044500d90d0ec5c708d9b81729727e465009f091 Mon Sep 17 00:00:00 2001 From: doshq Date: Fri, 11 Jul 2025 20:59:38 +0300 Subject: [PATCH 039/152] Add mongodb database --- backend/db.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 backend/db.py diff --git a/backend/db.py b/backend/db.py new file mode 100644 index 00000000..5ba75506 --- /dev/null +++ b/backend/db.py @@ -0,0 +1,13 @@ +import os +from pathlib import Path +from dotenv import load_dotenv +from motor.motor_asyncio import AsyncIOMotorClient + + +dotenv_path = Path('.env') +load_dotenv(dotenv_path=dotenv_path) +MONGO_URI = os.getenv("MONGO_URI") + +client = AsyncIOMotorClient(MONGO_URI) +db = client["visual-circuit-designer"] +users = db["Users"] From 1c7e0f64663175f668b6f985a50ad10979fb0749 Mon Sep 17 00:00:00 2001 From: doshq Date: Sat, 12 Jul 2025 15:17:34 +0300 Subject: [PATCH 040/152] Add auth test service, unit test on register --- backend/__init__.py | 0 backend/app/__init__.py | 0 backend/app/config.py | 15 ++++++++++++ backend/app/db.py | 6 +++++ backend/app/main.py | 41 ++++++++++++++++++++++++++++++++ backend/app/models.py | 12 ++++++++++ backend/app/schema.py | 26 ++++++++++++++++++++ backend/app/users/__init__.py | 0 backend/app/users/manager.py | 39 ++++++++++++++++++++++++++++++ backend/app/users/mongo_users.py | 40 +++++++++++++++++++++++++++++++ backend/db.py | 13 ---------- backend/tests/__init__.py | 0 backend/tests/test_register.py | 23 ++++++++++++++++++ 13 files changed, 202 insertions(+), 13 deletions(-) create mode 100644 backend/__init__.py create mode 100644 backend/app/__init__.py create mode 100644 backend/app/config.py create mode 100644 backend/app/db.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models.py create mode 100644 backend/app/schema.py create mode 100644 backend/app/users/__init__.py create mode 100644 backend/app/users/manager.py create mode 100644 backend/app/users/mongo_users.py delete mode 100644 backend/db.py create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/test_register.py diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 00000000..4bc3856a --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,15 @@ +import os +from pathlib import Path +from dotenv import load_dotenv + + +dotenv_path = Path('.env') +load_dotenv() + +MONGO_URI = os.getenv("MONGO_URI") +SECRET = os.getenv("SECRET") + +if not MONGO_URI: + raise ValueError("MONGO_URI not set in environment variables") +if not SECRET: + raise ValueError("SECRET not set in environment variables") \ No newline at end of file diff --git a/backend/app/db.py b/backend/app/db.py new file mode 100644 index 00000000..7c6bb4c0 --- /dev/null +++ b/backend/app/db.py @@ -0,0 +1,6 @@ +from ..app.config import MONGO_URI +from motor.motor_asyncio import AsyncIOMotorClient + +client = AsyncIOMotorClient(MONGO_URI) +db = client["visual-circuit-designer"] +user_collection = db["Users"] diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 00000000..cd7f9624 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,41 @@ +from fastapi import FastAPI, Depends +from fastapi_users import FastAPIUsers +from ..app.schema import UserCreate, UserRead, UserUpdate +from ..app.models import UserDB +from ..app.db import user_collection +from ..app.users.mongo_users import MongoUserDatabase +from ..app.users.manager import MyUserManager, auth_backend, get_jwt_strategy +from uuid import UUID + +user_db = MongoUserDatabase(user_collection) + +async def get_user_db(): + yield user_db + +async def get_user_manager(db=Depends(get_user_db)): + yield MyUserManager(db) + +fastapi_users = FastAPIUsers[UserDB, UUID]( + get_user_manager, + [auth_backend], +) + +app = FastAPI() + +app.include_router( + fastapi_users.get_auth_router(auth_backend), + prefix="/auth/jwt", + tags=["auth"] +) + +app.include_router( + fastapi_users.get_register_router(UserRead, UserCreate), + prefix="/auth", + tags=["auth"] +) + +app.include_router( + fastapi_users.get_users_router(UserRead, UserUpdate, UserDB), + prefix="/users", + tags=["users"] +) \ No newline at end of file diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 00000000..da013f97 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel, EmailStr, Field +from uuid import UUID, uuid4 + +class UserDB(BaseModel): + id: UUID = Field(default_factory=uuid4) + name: str + username: str + email: EmailStr + hashed_password: str + is_active: bool = True + is_superuser: bool = False + is_verified: bool = False \ No newline at end of file diff --git a/backend/app/schema.py b/backend/app/schema.py new file mode 100644 index 00000000..49cac4de --- /dev/null +++ b/backend/app/schema.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel, EmailStr +from typing import Optional +from uuid import UUID +from fastapi_users import schemas + + +class UserRead(schemas.BaseUser): + id: UUID + name: str + username: str + email: EmailStr + is_active: bool + + +class UserCreate(schemas.BaseUserCreate): + name: str + username: str + email: EmailStr + password: str + + +class UserUpdate(schemas.BaseUserUpdate): + name: Optional[str] = None + username: Optional[str] = None + email: Optional[EmailStr] = None + password: Optional[str] = None \ No newline at end of file diff --git a/backend/app/users/__init__.py b/backend/app/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/users/manager.py b/backend/app/users/manager.py new file mode 100644 index 00000000..e7ad78e8 --- /dev/null +++ b/backend/app/users/manager.py @@ -0,0 +1,39 @@ +from uuid import UUID, uuid4 +from fastapi_users.manager import BaseUserManager, UUIDIDMixin +from ..models import UserDB +from fastapi_users.authentication import JWTStrategy, AuthenticationBackend, CookieTransport +from ..config import SECRET +from ..schema import UserCreate + + +class MyUserManager(UUIDIDMixin, BaseUserManager[UserDB, UUID]): + async def on_after_register(self, user: UserDB, request=None): + print(f"User registered: {user}") + + async def create(self, user_create: UserCreate, safe: bool = False, request=None) -> UserDB: + # Ensure all required fields are included + user_id = uuid4() + + # Prepare user data + user_dict = user_create.model_dump() + user_dict["id"] = str(user_id) + user_dict["hashed_password"] = self.password_helper.hash(user_create.password) + + user_dict["is_active"] = True + user_dict["is_superuser"] = False + user_dict["is_verified"] = False + user_dict.pop("password", None) + + created_user = await self.user_db.create(user_dict) + return await self.get(created_user["id"]) + +def get_jwt_strategy() -> JWTStrategy: + return JWTStrategy(secret=SECRET, lifetime_seconds=3600) + +cookie_transport = CookieTransport(cookie_name="refresh_token", cookie_max_age=3600, cookie_secure=False) + +auth_backend = AuthenticationBackend( + name="jwt", + transport=cookie_transport, + get_strategy=get_jwt_strategy, +) \ No newline at end of file diff --git a/backend/app/users/mongo_users.py b/backend/app/users/mongo_users.py new file mode 100644 index 00000000..c41ae5cc --- /dev/null +++ b/backend/app/users/mongo_users.py @@ -0,0 +1,40 @@ +from fastapi_users.db.base import BaseUserDatabase +from ..models import UserDB +from motor.motor_asyncio import AsyncIOMotorCollection +from uuid import UUID, uuid4 +from typing import Optional, Any, Coroutine + + +class MongoUserDatabase(BaseUserDatabase[UserDB, UUID]): + def __init__(self, collection: AsyncIOMotorCollection): + self.collection = collection + + async def get(self, id: UUID) -> Optional[UserDB]: + user = await self.collection.find_one({"id": str(id)}) + return UserDB(**user) if user else None + + async def get_by_email(self, email: str) -> Optional[UserDB]: + user = await self.collection.find_one({"email": email}) + return self._convert_to_userdb(dict(user)) if user else None + + async def create(self, user: dict) -> dict: + if "id" not in user: + print("[mongo_users] User does not have an ID, generating one...") + user["id"] = str(uuid4()) + user.setdefault("is_active", True) + user.setdefault("is_superuser", False) + user.setdefault("is_verified", False) + await self.collection.insert_one(user) + return user + + async def update(self, user: UserDB) -> UserDB: + await self.collection.replace_one({"id": str(user.id)}, user.dict()) + return user + + async def delete(self, user: UserDB) -> None: + await self.collection.delete_one({"id": str(user.id)}) + + async def _convert_to_userdb(self, user_dict: dict) -> UserDB: + # Convert MongoDB document to UserDB + user_dict["id"] = UUID(user_dict["id"]) if isinstance(user_dict.get("id"), str) else user_dict.get("id") + return UserDB(**user_dict) \ No newline at end of file diff --git a/backend/db.py b/backend/db.py deleted file mode 100644 index 5ba75506..00000000 --- a/backend/db.py +++ /dev/null @@ -1,13 +0,0 @@ -import os -from pathlib import Path -from dotenv import load_dotenv -from motor.motor_asyncio import AsyncIOMotorClient - - -dotenv_path = Path('.env') -load_dotenv(dotenv_path=dotenv_path) -MONGO_URI = os.getenv("MONGO_URI") - -client = AsyncIOMotorClient(MONGO_URI) -db = client["visual-circuit-designer"] -users = db["Users"] diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/test_register.py b/backend/tests/test_register.py new file mode 100644 index 00000000..246cd1a3 --- /dev/null +++ b/backend/tests/test_register.py @@ -0,0 +1,23 @@ +import httpx +import pytest +from ..app.main import app +from ..app.db import user_collection + + +@pytest.mark.asyncio +async def test_register_user(): + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as ac: + response = await ac.post("/auth/register", json={ + "name": "Test User", + "username": "bruh", + "email": "test@example.com", + "password": "TestPassword123" + }) + + assert response.status_code == 201 + data = response.json() + assert "id" in data + + user = await user_collection.find_one({"email": "test@example.com"}) + assert user is not None \ No newline at end of file From 65bcda9e243747495bfd3a95a70d52817139b92f Mon Sep 17 00:00:00 2001 From: doshq Date: Sat, 12 Jul 2025 18:40:46 +0300 Subject: [PATCH 041/152] Add register, login functionality. Add multiple unit tests --- backend/app/db.py | 2 +- backend/app/models.py | 2 +- backend/app/schema.py | 2 +- backend/app/users/manager.py | 57 +++++++++++++++++-- backend/app/users/mongo_users.py | 32 +++++++++-- backend/tests/conftest.py | 32 +++++++++++ backend/tests/test_delete_user.py | 29 ++++++++++ backend/tests/test_get_curr_user.py | 25 ++++++++ backend/tests/test_login.py | 28 +++++++++ backend/tests/test_login_wrong_password.py | 14 +++++ backend/tests/test_password_validation.py | 15 +++++ backend/tests/test_protection.py | 8 +++ backend/tests/test_register.py | 27 ++++----- backend/tests/test_register_invalid_email.py | 14 +++++ .../tests/test_registered_duplicate_email.py | 16 ++++++ backend/tests/test_update_user.py | 30 ++++++++++ 16 files changed, 303 insertions(+), 30 deletions(-) create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_delete_user.py create mode 100644 backend/tests/test_get_curr_user.py create mode 100644 backend/tests/test_login.py create mode 100644 backend/tests/test_login_wrong_password.py create mode 100644 backend/tests/test_password_validation.py create mode 100644 backend/tests/test_protection.py create mode 100644 backend/tests/test_register_invalid_email.py create mode 100644 backend/tests/test_registered_duplicate_email.py create mode 100644 backend/tests/test_update_user.py diff --git a/backend/app/db.py b/backend/app/db.py index 7c6bb4c0..ce5b67f5 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -1,4 +1,4 @@ -from ..app.config import MONGO_URI +from backend.app.config import MONGO_URI from motor.motor_asyncio import AsyncIOMotorClient client = AsyncIOMotorClient(MONGO_URI) diff --git a/backend/app/models.py b/backend/app/models.py index da013f97..d97a7038 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -2,7 +2,7 @@ from uuid import UUID, uuid4 class UserDB(BaseModel): - id: UUID = Field(default_factory=uuid4) + id: UUID name: str username: str email: EmailStr diff --git a/backend/app/schema.py b/backend/app/schema.py index 49cac4de..26fbe5a3 100644 --- a/backend/app/schema.py +++ b/backend/app/schema.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, EmailStr +from pydantic import EmailStr from typing import Optional from uuid import UUID from fastapi_users import schemas diff --git a/backend/app/users/manager.py b/backend/app/users/manager.py index e7ad78e8..aa84fd9f 100644 --- a/backend/app/users/manager.py +++ b/backend/app/users/manager.py @@ -1,15 +1,54 @@ +from typing import Optional from uuid import UUID, uuid4 + +from fastapi.openapi.models import Response +from fastapi.security import OAuth2PasswordRequestForm from fastapi_users.manager import BaseUserManager, UUIDIDMixin -from ..models import UserDB +from backend.app.models import UserDB from fastapi_users.authentication import JWTStrategy, AuthenticationBackend, CookieTransport -from ..config import SECRET -from ..schema import UserCreate +from fastapi_users.authentication import BearerTransport +from fastapi_users.password import PasswordHelper +from backend.app.config import SECRET +from backend.app.schema import UserCreate +import logging + +logger = logging.getLogger(__name__) class MyUserManager(UUIDIDMixin, BaseUserManager[UserDB, UUID]): + reset_password_token_secret = SECRET + verification_token_secret = SECRET + password_helper = PasswordHelper() + async def on_after_register(self, user: UserDB, request=None): print(f"User registered: {user}") + async def authenticate( + self, credentials: OAuth2PasswordRequestForm + ) -> Optional[UserDB]: + logger.debug(f"Authenticating user: {credentials.username}") + try: + user = await self.get_by_email(credentials.username) + if user is None: + # Run the hasher to mitigate timing attack + self.password_helper.hash(credentials.password) + return None + + verified, updated_password_hash = self.password_helper.verify_and_update( + credentials.password, user.hashed_password + ) + if not verified: + return None + + if updated_password_hash is not None: + user.hashed_password = updated_password_hash + await self.user_db.update(user, {}) + + return user + except Exception: + # В случае любой ошибки возвращаем None + return None + async def create(self, user_create: UserCreate, safe: bool = False, request=None) -> UserDB: # Ensure all required fields are included user_id = uuid4() @@ -27,10 +66,18 @@ async def create(self, user_create: UserCreate, safe: bool = False, request=None created_user = await self.user_db.create(user_dict) return await self.get(created_user["id"]) + +class CustomCookieTransport(CookieTransport): + async def get_login_response(self, token: str) -> Response: + response = await super().get_login_response(token) + response.status_code = 200 # Меняем статус + return response + def get_jwt_strategy() -> JWTStrategy: - return JWTStrategy(secret=SECRET, lifetime_seconds=3600) + return JWTStrategy(secret=SECRET, lifetime_seconds=3600, algorithm="HS256") -cookie_transport = CookieTransport(cookie_name="refresh_token", cookie_max_age=3600, cookie_secure=False) +cookie_transport = CustomCookieTransport(cookie_name="refresh_token", cookie_max_age=3600, cookie_secure=False) +# bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") auth_backend = AuthenticationBackend( name="jwt", diff --git a/backend/app/users/mongo_users.py b/backend/app/users/mongo_users.py index c41ae5cc..a108a315 100644 --- a/backend/app/users/mongo_users.py +++ b/backend/app/users/mongo_users.py @@ -1,8 +1,8 @@ from fastapi_users.db.base import BaseUserDatabase -from ..models import UserDB +from backend.app.models import UserDB from motor.motor_asyncio import AsyncIOMotorCollection from uuid import UUID, uuid4 -from typing import Optional, Any, Coroutine +from typing import Optional, Dict, Any class MongoUserDatabase(BaseUserDatabase[UserDB, UUID]): @@ -15,7 +15,11 @@ async def get(self, id: UUID) -> Optional[UserDB]: async def get_by_email(self, email: str) -> Optional[UserDB]: user = await self.collection.find_one({"email": email}) - return self._convert_to_userdb(dict(user)) if user else None + if user: + # Ensure the ID is properly converted + user["id"] = str(user["id"]) if isinstance(user.get("id"), UUID) else user.get("id") + return UserDB(**user) + return None async def create(self, user: dict) -> dict: if "id" not in user: @@ -27,9 +31,25 @@ async def create(self, user: dict) -> dict: await self.collection.insert_one(user) return user - async def update(self, user: UserDB) -> UserDB: - await self.collection.replace_one({"id": str(user.id)}, user.dict()) - return user + async def update( + self, + user: UserDB, + update_dict: Dict[str, Any] = None, + ) -> UserDB: + if update_dict: + user_dict = user.model_dump() + user_dict.update(update_dict) + await self.collection.replace_one( + {"id": str(user.id)}, + user_dict + ) + return UserDB(**user_dict) + else: + await self.collection.replace_one( + {"id": str(user.id)}, + user.model_dump() + ) + return user async def delete(self, user: UserDB) -> None: await self.collection.delete_one({"id": str(user.id)}) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 00000000..0d839e3d --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,32 @@ +import httpx +import pytest +import pytest_asyncio + +from backend.app.db import user_collection +from backend.app.main import app + + +@pytest_asyncio.fixture +async def test_client(): + transport = httpx.ASGITransport(app=app) + return httpx.AsyncClient(transport=transport, base_url="http://test") + +@pytest_asyncio.fixture(autouse=True) +async def cleanup(test_client): + await user_collection.delete_many({}) + yield + await user_collection.delete_many({}) + await test_client.aclose() + +@pytest_asyncio.fixture +async def registered_user(test_client): + user_data = { + "name": "Test User", + "username": "testuser", + "email": "test@example.com", + "password": "TestPassword123" + } + response = await test_client.post("/auth/register", json=user_data) + assert response.status_code == 201 + + return user_data, response.json() \ No newline at end of file diff --git a/backend/tests/test_delete_user.py b/backend/tests/test_delete_user.py new file mode 100644 index 00000000..da68b412 --- /dev/null +++ b/backend/tests/test_delete_user.py @@ -0,0 +1,29 @@ +import pytest +from fastapi import status + + +@pytest.mark.asyncio +async def test_delete_user(test_client, registered_user): + _, user = registered_user + + # Login + login_response = await test_client.post("/auth/jwt/login", data={ + "username": user["email"], + "password": "TestPassword123" + }) + token = login_response.json()["access_token"] + + # Delete user + response = await test_client.delete( + f"/users/{user['id']}", + headers={"Authorization": f"Bearer {token}"} + ) + + assert response.status_code == status.HTTP_204_NO_CONTENT + + # Verify user is deleted + login_response = await test_client.post("/auth/jwt/login", data={ + "username": user["email"], + "password": "TestPassword123" + }) + assert login_response.status_code == status.HTTP_400_BAD_REQUEST \ No newline at end of file diff --git a/backend/tests/test_get_curr_user.py b/backend/tests/test_get_curr_user.py new file mode 100644 index 00000000..0689c775 --- /dev/null +++ b/backend/tests/test_get_curr_user.py @@ -0,0 +1,25 @@ +import pytest +from fastapi import status + + +@pytest.mark.asyncio +async def test_get_current_user(test_client, registered_user): + _, user = registered_user + + # First login to get token + login_response = await test_client.post("/auth/jwt/login", data={ + "username": user["email"], + "password": "TestPassword123" + }) + token = login_response.json()["access_token"] + + # Get current user + response = await test_client.get( + "/users/me", + headers={"Authorization": f"Bearer {token}"} + ) + + assert response.status_code == status.HTTP_200_OK + data = response.json() + assert data["email"] == user["email"] + assert data["username"] == user["username"] \ No newline at end of file diff --git a/backend/tests/test_login.py b/backend/tests/test_login.py new file mode 100644 index 00000000..3999bc5c --- /dev/null +++ b/backend/tests/test_login.py @@ -0,0 +1,28 @@ +import pytest +from fastapi import status + + +@pytest.mark.asyncio +async def test_login_success(test_client, registered_user): + user_data, user = registered_user + + # Используем form-data вместо json для OAuth2 + print("User in DB:", user) + print("Attempting login with:", user_data["email"], user_data["password"]) + + response = await test_client.post( + "/auth/jwt/login", + data={ + "username": user_data["email"], + "password": user_data["password"] + } + ) + + print("Response:", response.status_code, response.text) + + + assert response.status_code == status.HTTP_200_OK + print("Response:", response.text) + print("Cookies:", response.cookies) + assert "refresh_token" in response.cookies + # assert "access_token" in response.json() \ No newline at end of file diff --git a/backend/tests/test_login_wrong_password.py b/backend/tests/test_login_wrong_password.py new file mode 100644 index 00000000..a2732de4 --- /dev/null +++ b/backend/tests/test_login_wrong_password.py @@ -0,0 +1,14 @@ +import pytest +from fastapi import status + + +@pytest.mark.asyncio +async def test_login_wrong_password(test_client, registered_user): + user_data, _ = registered_user + response = await test_client.post("/auth/jwt/login", data={ + "username": user_data["email"], + "password": "WrongPassword" + }) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "LOGIN_BAD_CREDENTIALS" in response.json()["detail"] \ No newline at end of file diff --git a/backend/tests/test_password_validation.py b/backend/tests/test_password_validation.py new file mode 100644 index 00000000..16b36905 --- /dev/null +++ b/backend/tests/test_password_validation.py @@ -0,0 +1,15 @@ +import pytest +from fastapi import status + + +@pytest.mark.asyncio +async def test_register_weak_password(test_client): + response = await test_client.post("/auth/register", json={ + "name": "Weak User", + "username": "weak", + "email": "weak@example.com", + "password": "123" + }) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "password" in response.json()["detail"] \ No newline at end of file diff --git a/backend/tests/test_protection.py b/backend/tests/test_protection.py new file mode 100644 index 00000000..2aad84b3 --- /dev/null +++ b/backend/tests/test_protection.py @@ -0,0 +1,8 @@ +import pytest +from fastapi import status + + +@pytest.mark.asyncio +async def test_access_protected_route_without_token(test_client): + response = await test_client.get("/users/me") + assert response.status_code == status.HTTP_401_UNAUTHORIZED \ No newline at end of file diff --git a/backend/tests/test_register.py b/backend/tests/test_register.py index 246cd1a3..6900fdbe 100644 --- a/backend/tests/test_register.py +++ b/backend/tests/test_register.py @@ -1,23 +1,18 @@ -import httpx import pytest -from ..app.main import app -from ..app.db import user_collection +from fastapi import status @pytest.mark.asyncio -async def test_register_user(): - transport = httpx.ASGITransport(app=app) - async with httpx.AsyncClient(transport=transport, base_url="http://test") as ac: - response = await ac.post("/auth/register", json={ - "name": "Test User", - "username": "bruh", - "email": "test@example.com", - "password": "TestPassword123" - }) +async def test_register_user(test_client): + response = await test_client.post("/auth/register", json={ + "name": "New User", + "username": "newuser", + "email": "new@example.com", + "password": "NewPassword123" + }) - assert response.status_code == 201 + assert response.status_code == status.HTTP_201_CREATED data = response.json() assert "id" in data - - user = await user_collection.find_one({"email": "test@example.com"}) - assert user is not None \ No newline at end of file + assert data["email"] == "new@example.com" + assert data["username"] == "newuser" diff --git a/backend/tests/test_register_invalid_email.py b/backend/tests/test_register_invalid_email.py new file mode 100644 index 00000000..90fca109 --- /dev/null +++ b/backend/tests/test_register_invalid_email.py @@ -0,0 +1,14 @@ +import pytest +from fastapi import status + + +@pytest.mark.asyncio +async def test_register_invalid_email(test_client): + response = await test_client.post("/auth/register", json={ + "name": "Invalid User", + "username": "invalid", + "email": "not-an-email", + "password": "Password123" + }) + + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY \ No newline at end of file diff --git a/backend/tests/test_registered_duplicate_email.py b/backend/tests/test_registered_duplicate_email.py new file mode 100644 index 00000000..6a681995 --- /dev/null +++ b/backend/tests/test_registered_duplicate_email.py @@ -0,0 +1,16 @@ +import pytest +from fastapi import status + + +@pytest.mark.asyncio +async def test_register_duplicate_email(test_client, registered_user): + _, user = registered_user + response = await test_client.post("/auth/register", json={ + "name": "Duplicate User", + "username": "duplicate", + "email": user["email"], # Same email + "password": "AnotherPassword123" + }) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "email" in response.json()["detail"] \ No newline at end of file diff --git a/backend/tests/test_update_user.py b/backend/tests/test_update_user.py new file mode 100644 index 00000000..3acf2fba --- /dev/null +++ b/backend/tests/test_update_user.py @@ -0,0 +1,30 @@ +import pytest +from fastapi import status + + +@pytest.mark.asyncio +async def test_update_user(test_client, registered_user): + _, user = registered_user + + # Login + login_response = await test_client.post("/auth/jwt/login", data={ + "username": user["email"], + "password": "TestPassword123" + }) + token = login_response.json()["access_token"] + + # Update user + update_data = { + "name": "Updated Name", + "username": "updatedusername" + } + response = await test_client.patch( + f"/users/{user['id']}", + json=update_data, + headers={"Authorization": f"Bearer {token}"} + ) + + assert response.status_code == status.HTTP_200_OK + data = response.json() + assert data["name"] == update_data["name"] + assert data["username"] == update_data["username"] \ No newline at end of file From 2a083906b52563c4e36b2ca6e2bfe3c536596f0a Mon Sep 17 00:00:00 2001 From: doshq Date: Sat, 12 Jul 2025 23:03:08 +0300 Subject: [PATCH 042/152] Fix login --- backend/tests/test_login.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/tests/test_login.py b/backend/tests/test_login.py index 3999bc5c..2334318b 100644 --- a/backend/tests/test_login.py +++ b/backend/tests/test_login.py @@ -25,4 +25,3 @@ async def test_login_success(test_client, registered_user): print("Response:", response.text) print("Cookies:", response.cookies) assert "refresh_token" in response.cookies - # assert "access_token" in response.json() \ No newline at end of file From 99e6732533ab0c34f978e111af6795e2f28c71ba Mon Sep 17 00:00:00 2001 From: doshq Date: Sun, 13 Jul 2025 16:28:41 +0300 Subject: [PATCH 043/152] Intermediate stage --- backend/app/main.py | 34 ++++++++++++++++--- backend/app/users/manager.py | 20 ++++------- backend/app/users/mongo_users.py | 7 ++++ backend/tests/test_delete_user.py | 5 +++ backend/tests/test_login.py | 2 +- .../tests/test_registered_duplicate_email.py | 18 +++++++--- .../test_registered_duplicate_username.py | 21 ++++++++++++ 7 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 backend/tests/test_registered_duplicate_username.py diff --git a/backend/app/main.py b/backend/app/main.py index cd7f9624..4cd7514d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,10 +1,15 @@ -from fastapi import FastAPI, Depends +from contextlib import asynccontextmanager + +from fastapi import FastAPI, Depends, APIRouter from fastapi_users import FastAPIUsers +from pymongo.errors import DuplicateKeyError +from starlette.responses import JSONResponse + from ..app.schema import UserCreate, UserRead, UserUpdate from ..app.models import UserDB from ..app.db import user_collection from ..app.users.mongo_users import MongoUserDatabase -from ..app.users.manager import MyUserManager, auth_backend, get_jwt_strategy +from ..app.users.manager import MyUserManager, auth_backend from uuid import UUID user_db = MongoUserDatabase(user_collection) @@ -21,6 +26,19 @@ async def get_user_manager(db=Depends(get_user_db)): ) app = FastAPI() +router = APIRouter() + +@app.exception_handler(DuplicateKeyError) +async def mongo_duplicate_handler(request, exc): + detail = "Duplicate key error" + if "email" in str(exc): + detail = "Email already exists" + if "username" in str(exc): + detail = "Username already exists" + return JSONResponse( + status_code=400, + content={"detail": detail}, + ) app.include_router( fastapi_users.get_auth_router(auth_backend), @@ -35,7 +53,15 @@ async def get_user_manager(db=Depends(get_user_db)): ) app.include_router( - fastapi_users.get_users_router(UserRead, UserUpdate, UserDB), + fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"] -) \ No newline at end of file +) + +async def create_indexes(): + await user_collection.create_index("email", unique=True) + await user_collection.create_index("username", unique=True) + +@asynccontextmanager +async def startup(): + await create_indexes() \ No newline at end of file diff --git a/backend/app/users/manager.py b/backend/app/users/manager.py index aa84fd9f..0959b2b2 100644 --- a/backend/app/users/manager.py +++ b/backend/app/users/manager.py @@ -1,12 +1,12 @@ from typing import Optional from uuid import UUID, uuid4 -from fastapi.openapi.models import Response from fastapi.security import OAuth2PasswordRequestForm from fastapi_users.manager import BaseUserManager, UUIDIDMixin +from fastapi.responses import JSONResponse, Response + from backend.app.models import UserDB -from fastapi_users.authentication import JWTStrategy, AuthenticationBackend, CookieTransport -from fastapi_users.authentication import BearerTransport +from fastapi_users.authentication import JWTStrategy, AuthenticationBackend, CookieTransport, BearerTransport from fastapi_users.password import PasswordHelper from backend.app.config import SECRET from backend.app.schema import UserCreate @@ -46,7 +46,6 @@ async def authenticate( return user except Exception: - # В случае любой ошибки возвращаем None return None async def create(self, user_create: UserCreate, safe: bool = False, request=None) -> UserDB: @@ -66,21 +65,14 @@ async def create(self, user_create: UserCreate, safe: bool = False, request=None created_user = await self.user_db.create(user_dict) return await self.get(created_user["id"]) - -class CustomCookieTransport(CookieTransport): - async def get_login_response(self, token: str) -> Response: - response = await super().get_login_response(token) - response.status_code = 200 # Меняем статус - return response - def get_jwt_strategy() -> JWTStrategy: return JWTStrategy(secret=SECRET, lifetime_seconds=3600, algorithm="HS256") -cookie_transport = CustomCookieTransport(cookie_name="refresh_token", cookie_max_age=3600, cookie_secure=False) -# bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") +cookie_transport = CookieTransport(cookie_name="refresh_token", cookie_max_age=3600, cookie_secure=False) +bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") auth_backend = AuthenticationBackend( name="jwt", - transport=cookie_transport, + transport=bearer_transport, get_strategy=get_jwt_strategy, ) \ No newline at end of file diff --git a/backend/app/users/mongo_users.py b/backend/app/users/mongo_users.py index a108a315..cdbe4a89 100644 --- a/backend/app/users/mongo_users.py +++ b/backend/app/users/mongo_users.py @@ -51,6 +51,13 @@ async def update( ) return user + async def get_by_username(self, username: str) -> Optional[UserDB]: + user = await self.collection.find_one({"username": username}) + if user: + user["id"] = str(user["id"]) if isinstance(user.get("id"), UUID) else user.get("id") + return UserDB(**user) + return None + async def delete(self, user: UserDB) -> None: await self.collection.delete_one({"id": str(user.id)}) diff --git a/backend/tests/test_delete_user.py b/backend/tests/test_delete_user.py index da68b412..b7a523af 100644 --- a/backend/tests/test_delete_user.py +++ b/backend/tests/test_delete_user.py @@ -11,6 +11,11 @@ async def test_delete_user(test_client, registered_user): "username": user["email"], "password": "TestPassword123" }) + print("Login status:", login_response.status_code) + print("Login body:", login_response.text) + assert login_response.status_code == status.HTTP_200_OK + assert login_response is not None + assert "access_token" in login_response.json() token = login_response.json()["access_token"] # Delete user diff --git a/backend/tests/test_login.py b/backend/tests/test_login.py index 2334318b..7719a973 100644 --- a/backend/tests/test_login.py +++ b/backend/tests/test_login.py @@ -6,7 +6,6 @@ async def test_login_success(test_client, registered_user): user_data, user = registered_user - # Используем form-data вместо json для OAuth2 print("User in DB:", user) print("Attempting login with:", user_data["email"], user_data["password"]) @@ -24,4 +23,5 @@ async def test_login_success(test_client, registered_user): assert response.status_code == status.HTTP_200_OK print("Response:", response.text) print("Cookies:", response.cookies) + assert response.json()["access_token"] assert "refresh_token" in response.cookies diff --git a/backend/tests/test_registered_duplicate_email.py b/backend/tests/test_registered_duplicate_email.py index 6a681995..d97ebb68 100644 --- a/backend/tests/test_registered_duplicate_email.py +++ b/backend/tests/test_registered_duplicate_email.py @@ -5,12 +5,20 @@ @pytest.mark.asyncio async def test_register_duplicate_email(test_client, registered_user): _, user = registered_user + + await test_client.post("/auth/register", json={ + "email": "test@example.com", + "name": "new user", + "username": "testuser", + "password": "password" + }) + response = await test_client.post("/auth/register", json={ - "name": "Duplicate User", - "username": "duplicate", - "email": user["email"], # Same email - "password": "AnotherPassword123" + "email": "test@example.com", + "name": "super new user", + "username": "newuser", + "password": "password" }) assert response.status_code == status.HTTP_400_BAD_REQUEST - assert "email" in response.json()["detail"] \ No newline at end of file + assert "email" in response.json()["detail"].lower() \ No newline at end of file diff --git a/backend/tests/test_registered_duplicate_username.py b/backend/tests/test_registered_duplicate_username.py new file mode 100644 index 00000000..3b927eb8 --- /dev/null +++ b/backend/tests/test_registered_duplicate_username.py @@ -0,0 +1,21 @@ +import pytest + + +@pytest.mark.asyncio +async def test_register_duplicate_username(test_client): + await test_client.post("/auth/register", json={ + "email": "test@example.com", + "name": "Test user", + "username": "uniqueuser", + "password": "password" + }) + + response = await test_client.post("/auth/register", json={ + "email": "new@example.com", + "name": "New User", + "username": "uniqueuser", + "password": "password" + }) + + assert response.status_code == 400 + assert "username" in response.json()["detail"].lower() \ No newline at end of file From a718f3e547fc500181dd4617edffd49cd3a8a6a9 Mon Sep 17 00:00:00 2001 From: doshq Date: Sun, 13 Jul 2025 18:35:22 +0300 Subject: [PATCH 044/152] Both transports using now --- backend/app/main.py | 48 ++++++++++++++++++---- backend/app/users/manager.py | 33 ++++++++++++--- backend/tests/test_get_curr_user.py | 2 +- backend/tests/test_login.py | 10 ++++- backend/tests/test_login_wrong_password.py | 2 +- backend/tests/test_update_user.py | 2 +- 6 files changed, 80 insertions(+), 17 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 4cd7514d..6de2dec7 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,7 +1,14 @@ from contextlib import asynccontextmanager -from fastapi import FastAPI, Depends, APIRouter +from fastapi import FastAPI, Depends, APIRouter, HTTPException +from fastapi.responses import Response +from fastapi.security import OAuth2PasswordRequestForm +from fastapi import status from fastapi_users import FastAPIUsers +from fastapi_users.manager import UserManagerDependency, BaseUserManager +from fastapi_users.router import ErrorCode +from fastapi_users.router.common import ErrorModel +from pydantic import BaseModel from pymongo.errors import DuplicateKeyError from starlette.responses import JSONResponse @@ -9,7 +16,7 @@ from ..app.models import UserDB from ..app.db import user_collection from ..app.users.mongo_users import MongoUserDatabase -from ..app.users.manager import MyUserManager, auth_backend +from ..app.users.manager import MyUserManager, auth_backend, refresh_backend from uuid import UUID user_db = MongoUserDatabase(user_collection) @@ -22,7 +29,7 @@ async def get_user_manager(db=Depends(get_user_db)): fastapi_users = FastAPIUsers[UserDB, UUID]( get_user_manager, - [auth_backend], + [auth_backend, refresh_backend], ) app = FastAPI() @@ -40,11 +47,11 @@ async def mongo_duplicate_handler(request, exc): content={"detail": detail}, ) -app.include_router( - fastapi_users.get_auth_router(auth_backend), - prefix="/auth/jwt", - tags=["auth"] -) +# app.include_router( +# fastapi_users.get_auth_router(auth_backend), +# prefix="/auth/jwt", +# tags=["auth"] +# ) app.include_router( fastapi_users.get_register_router(UserRead, UserCreate), @@ -58,6 +65,31 @@ async def mongo_duplicate_handler(request, exc): tags=["users"] ) +@router.post("/login") +async def login( + response: Response, + credentials: OAuth2PasswordRequestForm = Depends(), + user_manager: BaseUserManager[UserDB, UUID] = Depends(get_user_manager), +): + user = await user_manager.authenticate(credentials) + + if not user or not user.is_active: + raise HTTPException(status_code=400, detail="LOGIN_BAD_CREDENTIALS") + + # Generate tokens + access_token = await auth_backend.get_strategy().write_token(user) + refresh_token = await refresh_backend.get_strategy().write_token(user) + + # Set cookie + refresh_backend.transport._set_login_cookie(response, refresh_token) + + return { + "access_token": access_token, + "token_type": "bearer", + } + +app.include_router(router, prefix="/auth", tags=["auth"]) + async def create_indexes(): await user_collection.create_index("email", unique=True) await user_collection.create_index("username", unique=True) diff --git a/backend/app/users/manager.py b/backend/app/users/manager.py index 0959b2b2..c669ef5e 100644 --- a/backend/app/users/manager.py +++ b/backend/app/users/manager.py @@ -65,14 +65,37 @@ async def create(self, user_create: UserCreate, safe: bool = False, request=None created_user = await self.user_db.create(user_dict) return await self.get(created_user["id"]) -def get_jwt_strategy() -> JWTStrategy: - return JWTStrategy(secret=SECRET, lifetime_seconds=3600, algorithm="HS256") - -cookie_transport = CookieTransport(cookie_name="refresh_token", cookie_max_age=3600, cookie_secure=False) +# Transport for access token (Bearer token in response) bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") +# Transport for refresh token (HTTP-only cookie) +cookie_transport = CookieTransport(cookie_name="refresh_token", cookie_max_age=86_400) + +# Стратегия для access token (JSON) +def get_access_strategy() -> JWTStrategy: + return JWTStrategy( + secret=SECRET, + lifetime_seconds=3600 * 24, # 24 часа + algorithm="HS256" + ) + +# Стратегия для refresh token (Cookie) +def get_refresh_strategy() -> JWTStrategy: + return JWTStrategy( + secret=SECRET, + lifetime_seconds=86400 * 30, # 30 дней + algorithm="HS256" + ) + +# Create authentication backends auth_backend = AuthenticationBackend( name="jwt", transport=bearer_transport, - get_strategy=get_jwt_strategy, + get_strategy=get_access_strategy, +) + +refresh_backend = AuthenticationBackend( + name="cookie", + transport=cookie_transport, + get_strategy=get_refresh_strategy, ) \ No newline at end of file diff --git a/backend/tests/test_get_curr_user.py b/backend/tests/test_get_curr_user.py index 0689c775..44720ef7 100644 --- a/backend/tests/test_get_curr_user.py +++ b/backend/tests/test_get_curr_user.py @@ -7,7 +7,7 @@ async def test_get_current_user(test_client, registered_user): _, user = registered_user # First login to get token - login_response = await test_client.post("/auth/jwt/login", data={ + login_response = await test_client.post("/auth/login", data={ "username": user["email"], "password": "TestPassword123" }) diff --git a/backend/tests/test_login.py b/backend/tests/test_login.py index 7719a973..0925b400 100644 --- a/backend/tests/test_login.py +++ b/backend/tests/test_login.py @@ -1,6 +1,8 @@ import pytest from fastapi import status +from backend.app.main import app + @pytest.mark.asyncio async def test_login_success(test_client, registered_user): @@ -9,8 +11,13 @@ async def test_login_success(test_client, registered_user): print("User in DB:", user) print("Attempting login with:", user_data["email"], user_data["password"]) + print("Registered routes:") + for route in app.routes: + if hasattr(route, "path"): + print(f"{route.path}") + response = await test_client.post( - "/auth/jwt/login", + "/auth/login", data={ "username": user_data["email"], "password": user_data["password"] @@ -25,3 +32,4 @@ async def test_login_success(test_client, registered_user): print("Cookies:", response.cookies) assert response.json()["access_token"] assert "refresh_token" in response.cookies + assert response.cookies["refresh_token"] != response.json()["access_token"] diff --git a/backend/tests/test_login_wrong_password.py b/backend/tests/test_login_wrong_password.py index a2732de4..fad4745d 100644 --- a/backend/tests/test_login_wrong_password.py +++ b/backend/tests/test_login_wrong_password.py @@ -5,7 +5,7 @@ @pytest.mark.asyncio async def test_login_wrong_password(test_client, registered_user): user_data, _ = registered_user - response = await test_client.post("/auth/jwt/login", data={ + response = await test_client.post("/auth/login", data={ "username": user_data["email"], "password": "WrongPassword" }) diff --git a/backend/tests/test_update_user.py b/backend/tests/test_update_user.py index 3acf2fba..142aaf73 100644 --- a/backend/tests/test_update_user.py +++ b/backend/tests/test_update_user.py @@ -7,7 +7,7 @@ async def test_update_user(test_client, registered_user): _, user = registered_user # Login - login_response = await test_client.post("/auth/jwt/login", data={ + login_response = await test_client.post("/auth/login", data={ "username": user["email"], "password": "TestPassword123" }) From d5d12fec1fd6577e9160afd9a672739e676de63b Mon Sep 17 00:00:00 2001 From: witch2256 Date: Mon, 14 Jul 2025 19:59:40 +0300 Subject: [PATCH 045/152] added messages if sth wrong typed in input fields --- UI/src/CSS/auth.css | 10 ++ UI/src/components/pages/auth.jsx | 40 ++++-- UI/src/components/pages/register.jsx | 177 +++++++++++++++++++-------- 3 files changed, 172 insertions(+), 55 deletions(-) diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index f6ee42fc..cf5e3987 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -57,6 +57,16 @@ color: var(--main-0); } +.error-message { + color: #ff4d4f; + text-align: left; + width: 20rem; + font-size: 0.8rem; + margin-top: 0.3rem; + margin-right: 11rem; + animation: fadeIn 0.3s ease-in; +} + .input-password-window { display: flex; align-items: center; diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index dabbc6af..3f2e4921 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -9,8 +9,10 @@ const EMAIL_REGEXP = const Auth = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const [isTouched, setIsTouched] = useState(false); const emailRef = useRef(null); + const [wasEmailFocused, setWasEmailFocused] = useState(false); + const [emailError, setEmailError] = useState(""); + const isValidEmail = EMAIL_REGEXP.test(email); @@ -23,11 +25,21 @@ const Auth = () => { }; useEffect(() => { - if (emailRef.current) { - const showError = isTouched && !EMAIL_REGEXP.test(email) && email !== ""; - emailRef.current.style.borderColor = showError ? "red" : "var(--main-5)"; + if (wasEmailFocused) { + const error = email.trim() !== "" && !isValidEmail + ? "Please enter a valid email address" + : ""; + setEmailError(error); + if (emailRef.current) { + emailRef.current.style.borderColor = error ? "red" : "var(--main-5)"; + } + } else { + setEmailError(""); + if (emailRef.current) { + emailRef.current.style.borderColor = "var(--main-5)"; + } } - }, [isValidEmail, isTouched, email]); + }, [isValidEmail, wasEmailFocused, email]); return (
@@ -37,12 +49,26 @@ const Auth = () => {
Enter email:
setIsTouched(true)} + onFocus={() => setWasEmailFocused(false)} + onBlur={() => { + if (email.trim() !== "") { + setWasEmailFocused(true); + } else { + setWasEmailFocused(false); + } + }} placeholder="myEmail@example.com" /> + {wasEmailFocused && emailError && ( +
+ {emailError} +
+ )}
Enter password:
()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; -const USERNAME_REGEXP = /^[a-zA-Z0-9._-]{4,25}$/; const Auth = () => { + // Состояния для полей формы const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const [isTouched, setIsTouched] = useState(false); - const emailRef = useRef(null); const [username, setUsername] = useState(""); - const [isUsernameTouched, setIsUsernameTouched] = useState(false); - const usernameRef = useRef(null); - const [confirmPassword, setConfirmPassword] = useState(""); - const [isConfirmPasswordTouched, setIsConfirmPasswordTouched] = - useState(false); + + // Состояния для валидации + const [emailError, setEmailError] = useState(""); + const [wasEmailFocused, setWasEmailFocused] = useState(false); + + const [usernameError, setUsernameError] = useState(""); + const [wasUsernameFocused, setWasUsernameFocused] = useState(false); + + const [wasConfirmPasswordFocused, setWasConfirmPasswordFocused] = useState(false); + + // Рефы для элементов + const emailRef = useRef(null); + const usernameRef = useRef(null); const passwordRef = useRef(null); const confirmPasswordRef = useRef(null); - const isValidUsername = USERNAME_REGEXP.test(username); - + // Валидация email const isValidEmail = EMAIL_REGEXP.test(email); + // Валидация username + const validateUsername = () => { + if (username.trim() === "") return ""; + + if (username.length < 4 || username.length > 25) { + return "Username length must be 4-25 characters"; + } + + if (!/^[a-zA-Z0-9._-]*$/.test(username)) { + return "Username contains invalid character"; + } + + return ""; + }; + + // Валидация паролей + const passwordsMatch = password === confirmPassword; + + // Обработчики изменений полей const handleUsernameChange = (e) => { setUsername(e.target.value); }; @@ -42,45 +66,61 @@ const Auth = () => { setConfirmPassword(e.target.value); }; - const passwordsMatch = password === confirmPassword; - + // Эффект для валидации и стилизации полей useEffect(() => { - if (usernameRef.current) { - const showError = - isUsernameTouched && !isValidUsername && username !== ""; - usernameRef.current.style.borderColor = showError - ? "red" - : "var(--main-5)"; - } - - if (emailRef.current) { - const showEmailError = isTouched && !isValidEmail && email !== ""; - emailRef.current.style.borderColor = showEmailError - ? "red" - : "var(--main-5)"; + // Валидация username + if (wasUsernameFocused) { + const error = validateUsername(); + setUsernameError(error); + if (usernameRef.current) { + usernameRef.current.style.borderColor = error ? "red" : "var(--main-5)"; + } + } else { + setUsernameError(""); + if (usernameRef.current) { + usernameRef.current.style.borderColor = "var(--main-5)"; + } } - if (passwordRef.current) { - passwordRef.current.style.borderColor = "var(--main-5)"; + // Валидация email + if (wasEmailFocused) { + const error = email.trim() !== "" && !isValidEmail + ? "Please enter a valid email address" + : ""; + setEmailError(error); + if (emailRef.current) { + emailRef.current.style.borderColor = error ? "red" : "var(--main-5)"; + } + } else { + setEmailError(""); + if (emailRef.current) { + emailRef.current.style.borderColor = "var(--main-5)"; + } } - if (confirmPasswordRef.current) { - const showError = - isConfirmPasswordTouched && !passwordsMatch && confirmPassword !== ""; - confirmPasswordRef.current.style.borderColor = showError - ? "red" - : "var(--main-5)"; + // Валидация подтверждения пароля + if (wasConfirmPasswordFocused) { + const error = confirmPassword.trim() !== "" && !passwordsMatch + ? "Passwords do not match" + : ""; + if (confirmPasswordRef.current) { + confirmPasswordRef.current.style.borderColor = error ? "red" : "var(--main-5)"; + } + } else { + if (confirmPasswordRef.current) { + confirmPasswordRef.current.style.borderColor = "var(--main-5)"; + } } }, [ - isValidUsername, - isUsernameTouched, username, - isValidEmail, - isTouched, + wasUsernameFocused, email, - passwordsMatch, - isConfirmPasswordTouched, + wasEmailFocused, + isValidEmail, + password, confirmPassword, + wasConfirmPasswordFocused, + passwordsMatch, ]); return ( @@ -88,41 +128,70 @@ const Auth = () => {
Registration
+ {/* Поле username */}
Username
setIsUsernameTouched(true)} + onFocus={() => setWasUsernameFocused(false)} + onBlur={() => { + if (username.trim() !== "") { + setWasUsernameFocused(true); + } else { + setWasUsernameFocused(false); + } + }} /> + {wasUsernameFocused && usernameError && ( +
+ {usernameError} +
+ )} + {/* Поле email */}
Email
setIsTouched(true)} + onFocus={() => setWasEmailFocused(false)} + onBlur={() => { + if (email.trim() !== "") { + setWasEmailFocused(true); + } else { + setWasEmailFocused(false); + } + }} placeholder="myEmail@example.com" /> + {wasEmailFocused && emailError && ( +
+ {emailError} +
+ )} + {/* Поля пароля */}
Password
+
Confirm password:
{ type="password" value={confirmPassword} onChange={handleConfirmPasswordChange} - onBlur={() => setIsConfirmPasswordTouched(true)} + onFocus={() => setWasConfirmPasswordFocused(false)} + onBlur={() => { + if (confirmPassword.trim() !== "") { + setWasConfirmPasswordFocused(true); + } else { + setWasConfirmPasswordFocused(false); + } + }} /> + {wasConfirmPasswordFocused && !passwordsMatch && confirmPassword !== "" && ( +
+ Passwords do not match +
+ )}
{ ); }; -export default Auth; +export default Auth; \ No newline at end of file From cb08887ab397dbe17aed43707dd5ce4280b5b272 Mon Sep 17 00:00:00 2001 From: witch2256 Date: Mon, 14 Jul 2025 22:31:29 +0300 Subject: [PATCH 046/152] It is forbidden to enter/register if not all or incorrect data is specified in the fields --- UI/src/CSS/reg.css | 6 + UI/src/components/pages/auth.jsx | 108 +++++++++----- UI/src/components/pages/register.jsx | 206 ++++++++++++++------------- 3 files changed, 191 insertions(+), 129 deletions(-) diff --git a/UI/src/CSS/reg.css b/UI/src/CSS/reg.css index c8e2f724..d6d386e1 100644 --- a/UI/src/CSS/reg.css +++ b/UI/src/CSS/reg.css @@ -90,6 +90,12 @@ color: var(--main-0); } +.input-username-window.invalid, +.input-email-window.invalid, +.input-password-window.invalid { + border-color: red; +} + .input-confirm-password-text { display: flex; margin-top: 1.5rem; diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index 3f2e4921..6ae1e420 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -1,45 +1,76 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import "../../CSS/auth.css"; import "../../CSS/variables.css"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; // Добавлен useNavigate const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; const Auth = () => { + const navigate = useNavigate(); // Используем хук + + // Состояния полей формы const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const emailRef = useRef(null); - const [wasEmailFocused, setWasEmailFocused] = useState(false); + + // Состояния валидации const [emailError, setEmailError] = useState(""); + const [wasEmailFocused, setWasEmailFocused] = useState(false); + const [passwordError, setPasswordError] = useState(""); + const [wasPasswordFocused, setWasPasswordFocused] = useState(false); - const isValidEmail = EMAIL_REGEXP.test(email); + // Функции валидации + const validateEmail = useCallback(() => { + if (email.trim() === "") { + return "Email is required"; + } + if (!EMAIL_REGEXP.test(email)) { + return "Please enter a valid email address"; + } + return ""; + }, [email]); - const handleEmailChange = (e) => { - setEmail(e.target.value); - }; + const validatePassword = useCallback(() => { + if (password.trim() === "") { + return "Password is required"; + } + return ""; + }, [password]); - const handlePasswordChange = (e) => { - setPassword(e.target.value); - }; + // Обработчики изменений + const handleEmailChange = (e) => setEmail(e.target.value); + const handlePasswordChange = (e) => setPassword(e.target.value); + // Эффекты валидации useEffect(() => { if (wasEmailFocused) { - const error = email.trim() !== "" && !isValidEmail - ? "Please enter a valid email address" - : ""; - setEmailError(error); - if (emailRef.current) { - emailRef.current.style.borderColor = error ? "red" : "var(--main-5)"; - } - } else { - setEmailError(""); - if (emailRef.current) { - emailRef.current.style.borderColor = "var(--main-5)"; - } + setEmailError(validateEmail()); } - }, [isValidEmail, wasEmailFocused, email]); + }, [email, wasEmailFocused, validateEmail]); + + useEffect(() => { + if (wasPasswordFocused) { + setPasswordError(validatePassword()); + } + }, [password, wasPasswordFocused, validatePassword]); + + // Обработчик входа + const handleLogin = () => { + // Активируем проверку всех полей + setWasEmailFocused(true); + setWasPasswordFocused(true); + + const emailErr = validateEmail(); + const passwordErr = validatePassword(); + + setEmailError(emailErr); + setPasswordError(passwordErr); + + if (!emailErr && !passwordErr) { + navigate('/profile'); + } + }; return (
@@ -48,7 +79,6 @@ const Auth = () => {
Enter email:
{ onBlur={() => { if (email.trim() !== "") { setWasEmailFocused(true); - } else { - setWasEmailFocused(false); } }} placeholder="myEmail@example.com" @@ -72,19 +100,33 @@ const Auth = () => {
Enter password:
setWasPasswordFocused(false)} + onBlur={() => { + if (password.trim() !== "") { + setWasPasswordFocused(true); + } + }} /> + {wasPasswordFocused && passwordError && ( +
+ {passwordError} +
+ )}
- Log in - + +
Have no account?
Register @@ -94,4 +136,4 @@ const Auth = () => { ); }; -export default Auth; +export default Auth; \ No newline at end of file diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index 17186c30..dc5686f7 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -1,128 +1,143 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import "../../CSS/reg.css"; import "../../CSS/variables.css"; -import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; const Auth = () => { - // Состояния для полей формы + const navigate = useNavigate(); + + // Состояния полей формы const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [username, setUsername] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); - // Состояния для валидации + // Состояния валидации const [emailError, setEmailError] = useState(""); const [wasEmailFocused, setWasEmailFocused] = useState(false); const [usernameError, setUsernameError] = useState(""); const [wasUsernameFocused, setWasUsernameFocused] = useState(false); - const [wasConfirmPasswordFocused, setWasConfirmPasswordFocused] = useState(false); - - // Рефы для элементов - const emailRef = useRef(null); - const usernameRef = useRef(null); - const passwordRef = useRef(null); - const confirmPasswordRef = useRef(null); + const [passwordError, setPasswordError] = useState(""); + const [wasPasswordFocused, setWasPasswordFocused] = useState(false); - // Валидация email - const isValidEmail = EMAIL_REGEXP.test(email); - - // Валидация username - const validateUsername = () => { - if (username.trim() === "") return ""; + const [confirmPasswordError, setConfirmPasswordError] = useState(""); + const [wasConfirmPasswordFocused, setWasConfirmPasswordFocused] = useState(false); + // Функции валидации + const validateUsername = useCallback(() => { + if (username.trim() === "") { + return "Username is required"; + } if (username.length < 4 || username.length > 25) { return "Username length must be 4-25 characters"; } - if (!/^[a-zA-Z0-9._-]*$/.test(username)) { return "Username contains invalid character"; } - return ""; - }; - - // Валидация паролей - const passwordsMatch = password === confirmPassword; + }, [username]); - // Обработчики изменений полей - const handleUsernameChange = (e) => { - setUsername(e.target.value); - }; + const validateEmail = useCallback(() => { + if (email.trim() === "") { + return "Email is required"; + } + if (!EMAIL_REGEXP.test(email)) { + return "Please enter a valid email address"; + } + return ""; + }, [email]); - const handleEmailChange = (e) => { - setEmail(e.target.value); - }; + const validatePassword = useCallback(() => { + if (password.trim() === "") { + return "Password is required"; + } + if (password.length < 8 || password.length > 16) { + return "Password must be 8-16 characters"; + } + if (!/^[a-zA-Z0-9]*$/.test(password)) { + return "Password can only contain Latin letters and numbers"; + } + return ""; + }, [password]); - const handlePasswordChange = (e) => { - setPassword(e.target.value); - }; + const validateConfirmPassword = useCallback(() => { + if (confirmPassword.trim() === "") { + return "Confirm password is required"; + } + if (password !== confirmPassword) { + return "Passwords do not match"; + } + return ""; + }, [confirmPassword, password]); - const handleConfirmPasswordChange = (e) => { - setConfirmPassword(e.target.value); - }; + // Обработчики изменений + const handleUsernameChange = (e) => setUsername(e.target.value); + const handleEmailChange = (e) => setEmail(e.target.value); + const handlePasswordChange = (e) => setPassword(e.target.value); + const handleConfirmPasswordChange = (e) => setConfirmPassword(e.target.value); - // Эффект для валидации и стилизации полей + // Эффект валидации useEffect(() => { - // Валидация username if (wasUsernameFocused) { - const error = validateUsername(); - setUsernameError(error); - if (usernameRef.current) { - usernameRef.current.style.borderColor = error ? "red" : "var(--main-5)"; - } - } else { - setUsernameError(""); - if (usernameRef.current) { - usernameRef.current.style.borderColor = "var(--main-5)"; - } + setUsernameError(validateUsername()); } - // Валидация email if (wasEmailFocused) { - const error = email.trim() !== "" && !isValidEmail - ? "Please enter a valid email address" - : ""; - setEmailError(error); - if (emailRef.current) { - emailRef.current.style.borderColor = error ? "red" : "var(--main-5)"; - } - } else { - setEmailError(""); - if (emailRef.current) { - emailRef.current.style.borderColor = "var(--main-5)"; - } + setEmailError(validateEmail()); + } + + if (wasPasswordFocused) { + setPasswordError(validatePassword()); } - // Валидация подтверждения пароля if (wasConfirmPasswordFocused) { - const error = confirmPassword.trim() !== "" && !passwordsMatch - ? "Passwords do not match" - : ""; - if (confirmPasswordRef.current) { - confirmPasswordRef.current.style.borderColor = error ? "red" : "var(--main-5)"; - } - } else { - if (confirmPasswordRef.current) { - confirmPasswordRef.current.style.borderColor = "var(--main-5)"; - } + setConfirmPasswordError(validateConfirmPassword()); } }, [ username, wasUsernameFocused, email, wasEmailFocused, - isValidEmail, password, + wasPasswordFocused, confirmPassword, wasConfirmPasswordFocused, - passwordsMatch, + validateUsername, + validateEmail, + validatePassword, + validateConfirmPassword ]); + // Обработчик регистрации + const handleRegister = () => { + setWasUsernameFocused(true); + setWasEmailFocused(true); + setWasPasswordFocused(true); + setWasConfirmPasswordFocused(true); + + const errors = { + username: validateUsername(), + email: validateEmail(), + password: validatePassword(), + confirmPassword: validateConfirmPassword(), + }; + + setUsernameError(errors.username); + setEmailError(errors.email); + setPasswordError(errors.password); + setConfirmPasswordError(errors.confirmPassword); + + const hasErrors = Object.values(errors).some(error => error !== ""); + if (!hasErrors) { + navigate('/profile'); + } + }; + return (
@@ -131,7 +146,6 @@ const Auth = () => { {/* Поле username */}
Username
{ onBlur={() => { if (username.trim() !== "") { setWasUsernameFocused(true); - } else { - setWasUsernameFocused(false); } }} /> @@ -155,7 +167,6 @@ const Auth = () => { {/* Поле email */}
Email
{ onBlur={() => { if (email.trim() !== "") { setWasEmailFocused(true); - } else { - setWasEmailFocused(false); } }} placeholder="myEmail@example.com" @@ -180,22 +189,29 @@ const Auth = () => { {/* Поля пароля */}
Password
setWasPasswordFocused(false)} + onBlur={() => { + if (password.trim() !== "") { + setWasPasswordFocused(true); + } + }} /> + {wasPasswordFocused && passwordError && ( +
+ {passwordError} +
+ )}
Confirm password:
{ onBlur={() => { if (confirmPassword.trim() !== "") { setWasConfirmPasswordFocused(true); - } else { - setWasConfirmPasswordFocused(false); } }} /> - {wasConfirmPasswordFocused && !passwordsMatch && confirmPassword !== "" && ( + {wasConfirmPasswordFocused && confirmPasswordError && (
- Passwords do not match + {confirmPasswordError}
)}
- Register - +
); From fd3263c363d86eeab9624f5cc417a85257a987a0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:31:50 +0000 Subject: [PATCH 047/152] Automated formatting --- UI/src/components/pages/auth.jsx | 17 +++++------------ UI/src/components/pages/register.jsx | 24 +++++++++--------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index 6ae1e420..ea7bc4a7 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -68,7 +68,7 @@ const Auth = () => { setPasswordError(passwordErr); if (!emailErr && !passwordErr) { - navigate('/profile'); + navigate("/profile"); } }; @@ -93,9 +93,7 @@ const Auth = () => { placeholder="myEmail@example.com" /> {wasEmailFocused && emailError && ( -
- {emailError} -
+
{emailError}
)}
Enter password:
@@ -114,16 +112,11 @@ const Auth = () => { }} /> {wasPasswordFocused && passwordError && ( -
- {passwordError} -
+
{passwordError}
)}
- @@ -136,4 +129,4 @@ const Auth = () => { ); }; -export default Auth; \ No newline at end of file +export default Auth; diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index dc5686f7..54ab0f49 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -26,7 +26,8 @@ const Auth = () => { const [wasPasswordFocused, setWasPasswordFocused] = useState(false); const [confirmPasswordError, setConfirmPasswordError] = useState(""); - const [wasConfirmPasswordFocused, setWasConfirmPasswordFocused] = useState(false); + const [wasConfirmPasswordFocused, setWasConfirmPasswordFocused] = + useState(false); // Функции валидации const validateUsername = useCallback(() => { @@ -110,7 +111,7 @@ const Auth = () => { validateUsername, validateEmail, validatePassword, - validateConfirmPassword + validateConfirmPassword, ]); // Обработчик регистрации @@ -132,9 +133,9 @@ const Auth = () => { setPasswordError(errors.password); setConfirmPasswordError(errors.confirmPassword); - const hasErrors = Object.values(errors).some(error => error !== ""); + const hasErrors = Object.values(errors).some((error) => error !== ""); if (!hasErrors) { - navigate('/profile'); + navigate("/profile"); } }; @@ -159,9 +160,7 @@ const Auth = () => { }} /> {wasUsernameFocused && usernameError && ( -
- {usernameError} -
+
{usernameError}
)} {/* Поле email */} @@ -181,9 +180,7 @@ const Auth = () => { placeholder="myEmail@example.com" /> {wasEmailFocused && emailError && ( -
- {emailError} -
+
{emailError}
)} {/* Поля пароля */} @@ -230,10 +227,7 @@ const Auth = () => { )}
-
@@ -241,4 +235,4 @@ const Auth = () => { ); }; -export default Auth; \ No newline at end of file +export default Auth; From 0930f8f32ccda6df58780840482ed60bbc9336c5 Mon Sep 17 00:00:00 2001 From: witch2256 Date: Mon, 14 Jul 2025 22:43:36 +0300 Subject: [PATCH 048/152] Log in button on editor fixed --- UI/src/CSS/auth.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index cf5e3987..df0074c2 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -129,7 +129,7 @@ .login-button { position: fixed; - top: 3.05rem; + top: calc(0.5rem + 5vh); left: 4.97rem; width: 4rem; height: 2rem; From e12c68b8d78078c35260b1f47514685ff21a4b4e Mon Sep 17 00:00:00 2001 From: doshq Date: Tue, 15 Jul 2025 10:29:39 +0300 Subject: [PATCH 049/152] Add /auth/verify handler for token verification --- backend/app/main.py | 23 +++++++++++++---------- backend/app/users/manager.py | 16 ++++++++-------- backend/tests/test_verify_token.py | 27 +++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 18 deletions(-) create mode 100644 backend/tests/test_verify_token.py diff --git a/backend/app/main.py b/backend/app/main.py index 6de2dec7..7fd1e98c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,13 +2,10 @@ from fastapi import FastAPI, Depends, APIRouter, HTTPException from fastapi.responses import Response -from fastapi.security import OAuth2PasswordRequestForm +from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer from fastapi import status from fastapi_users import FastAPIUsers from fastapi_users.manager import UserManagerDependency, BaseUserManager -from fastapi_users.router import ErrorCode -from fastapi_users.router.common import ErrorModel -from pydantic import BaseModel from pymongo.errors import DuplicateKeyError from starlette.responses import JSONResponse @@ -32,6 +29,10 @@ async def get_user_manager(db=Depends(get_user_db)): [auth_backend, refresh_backend], ) +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/jwt/login") +get_current_user = fastapi_users.current_user(active=False, verified=False) + + app = FastAPI() router = APIRouter() @@ -47,12 +48,6 @@ async def mongo_duplicate_handler(request, exc): content={"detail": detail}, ) -# app.include_router( -# fastapi_users.get_auth_router(auth_backend), -# prefix="/auth/jwt", -# tags=["auth"] -# ) - app.include_router( fastapi_users.get_register_router(UserRead, UserCreate), prefix="/auth", @@ -88,6 +83,14 @@ async def login( "token_type": "bearer", } +@router.get("/verify", tags=["auth"]) +async def verify_access_token(user: UserDB = Depends(get_current_user)): + return { + "valid": True, + "user_id": str(user.id), + "email": user.email, + } + app.include_router(router, prefix="/auth", tags=["auth"]) async def create_indexes(): diff --git a/backend/app/users/manager.py b/backend/app/users/manager.py index c669ef5e..ee077908 100644 --- a/backend/app/users/manager.py +++ b/backend/app/users/manager.py @@ -3,7 +3,6 @@ from fastapi.security import OAuth2PasswordRequestForm from fastapi_users.manager import BaseUserManager, UUIDIDMixin -from fastapi.responses import JSONResponse, Response from backend.app.models import UserDB from fastapi_users.authentication import JWTStrategy, AuthenticationBackend, CookieTransport, BearerTransport @@ -65,26 +64,27 @@ async def create(self, user_create: UserCreate, safe: bool = False, request=None created_user = await self.user_db.create(user_dict) return await self.get(created_user["id"]) -# Transport for access token (Bearer token in response) +# Transport for access token (Bearer) bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") # Transport for refresh token (HTTP-only cookie) cookie_transport = CookieTransport(cookie_name="refresh_token", cookie_max_age=86_400) -# Стратегия для access token (JSON) def get_access_strategy() -> JWTStrategy: return JWTStrategy( secret=SECRET, - lifetime_seconds=3600 * 24, # 24 часа - algorithm="HS256" + lifetime_seconds=3600 * 24, # 24 hours + # lifetime_seconds=2, # For expiration tests + algorithm="HS256", + token_audience=["fastapi-users:auth"] ) -# Стратегия для refresh token (Cookie) def get_refresh_strategy() -> JWTStrategy: return JWTStrategy( secret=SECRET, - lifetime_seconds=86400 * 30, # 30 дней - algorithm="HS256" + lifetime_seconds=86400 * 30, # 30 days + algorithm="HS256", + token_audience = ["fastapi-users:auth"] ) # Create authentication backends diff --git a/backend/tests/test_verify_token.py b/backend/tests/test_verify_token.py new file mode 100644 index 00000000..99ea3b02 --- /dev/null +++ b/backend/tests/test_verify_token.py @@ -0,0 +1,27 @@ +import pytest +from fastapi import status + +@pytest.mark.asyncio +async def test_verify_access_token(test_client, registered_user): + user_data, user = registered_user + + login_response = await test_client.post( + "/auth/login", + data={ + "username": user_data["email"], + "password": user_data["password"] + } + ) + + response = await test_client.get( + "/auth/verify", + headers={"Authorization": f"Bearer {login_response.json()['access_token']}"} + ) + assert response.status_code == status.HTTP_200_OK + + invalid_token = login_response.json()["access_token"][::-1] + response = await test_client.get( + "/auth/verify", + headers={"Authorization": f"Bearer {invalid_token}"} + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED From 2b705a3f548f44ec671e40b2a189e7c47ec896b9 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Tue, 15 Jul 2025 19:55:42 +0300 Subject: [PATCH 050/152] test --- UI/assets/circuits-icons.jsx | 2 +- UI/src/CSS/App.css | 21 ------------------- UI/src/CSS/dnd.css | 7 +++---- .../components/circuits/IOelemnts/button.jsx | 2 +- UI/src/components/circuits/IOelemnts/led.jsx | 2 +- .../components/circuits/IOelemnts/switch.jsx | 2 +- .../codeComponents/LogicGateBase.jsx | 11 +++++++--- UI/src/components/hooks/useRotatedNode.jsx | 4 ---- UI/src/components/pages/mainPage.jsx | 2 +- 9 files changed, 16 insertions(+), 37 deletions(-) diff --git a/UI/assets/circuits-icons.jsx b/UI/assets/circuits-icons.jsx index b8160940..fc159adc 100644 --- a/UI/assets/circuits-icons.jsx +++ b/UI/assets/circuits-icons.jsx @@ -1,5 +1,5 @@ export const IconAND = ({ SVGClassName, style }) => ( - + Layer 1 +

Button

+

LED

+

Switch

- +
+ {/* Handles */} {handleConfigs.map(({ id: handleId, type, position }) => ( @@ -68,7 +73,7 @@ function LogicGateBase({ connections={handleId.slice(0, 2) === "in" ? 1 : undefined} /> ))} - +
); } diff --git a/UI/src/components/hooks/useRotatedNode.jsx b/UI/src/components/hooks/useRotatedNode.jsx index 4fd26e36..c092fd7b 100644 --- a/UI/src/components/hooks/useRotatedNode.jsx +++ b/UI/src/components/hooks/useRotatedNode.jsx @@ -74,10 +74,6 @@ export const useRotatedNode = (id, rotation, originalWidth, originalHeight) => { style={{ width: rotatedWidth, height: rotatedHeight, - display: "flex", - alignItems: "center", - justifyContent: "center", - position: "relative", }} >
{ setMenu(null); setOpenSettings(false); From 9f91fc8d770ee7520fa33ba585dbcafea785c64a Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Tue, 15 Jul 2025 20:16:40 +0300 Subject: [PATCH 051/152] logic gates hitbox fix --- UI/assets/circuits-icons.jsx | 62 ++++++++++++++++++------------------ UI/src/CSS/dnd.css | 2 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/UI/assets/circuits-icons.jsx b/UI/assets/circuits-icons.jsx index fc159adc..1c13cb7b 100644 --- a/UI/assets/circuits-icons.jsx +++ b/UI/assets/circuits-icons.jsx @@ -1,5 +1,5 @@ export const IconAND = ({ SVGClassName, style }) => ( - + Layer 1 ( ); export const IconOR = ({ SVGClassName, style }) => ( - + Layer 1 ( ); export const IconNAND = ({ SVGClassName, style }) => ( - + Layer 1 ( ); export const IconNOR = ({ SVGClassName, style }) => ( - + Layer 1 ( ); export const IconXOR = ({ SVGClassName, style }) => ( - + Layer 1 ( ); export const IconNOT = ({ SVGClassName, style }) => ( - + Layer 1 ( ); export const IconInput = ({ SVGClassName, style }) => ( - + Layer 1 ( ); export const IconOutput = ({ SVGClassName, style }) => ( - + Layer 1 ( ); -export const IconSwitchOn = ({ SVGClassName, style }) => ( - - - -); - -export const IconSwitchOff = ({ SVGClassName }) => ( - - - -); - +// export const IconSwitchOn = ({ SVGClassName, style }) => ( +// +// +// +// ); +// +// export const IconSwitchOff = ({ SVGClassName }) => ( +// +// +// +// ); +// // const IconXNOR = ({ SVGClassName }) => ( // // diff --git a/UI/src/CSS/dnd.css b/UI/src/CSS/dnd.css index dc101c02..ed1c7fbb 100644 --- a/UI/src/CSS/dnd.css +++ b/UI/src/CSS/dnd.css @@ -48,7 +48,7 @@ } .logic-gate-icon { - margin: -2% -14% 0 -14%; + /*margin: -2% -14% 0 -14%;*/ color: var(--main-0); object-fit: cover; } From ee8599f13e8a841ecf548f483473ad53beea5514 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:17:00 +0000 Subject: [PATCH 052/152] Automated formatting --- UI/src/components/codeComponents/LogicGateBase.jsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/UI/src/components/codeComponents/LogicGateBase.jsx b/UI/src/components/codeComponents/LogicGateBase.jsx index fed39e15..b81a17b6 100644 --- a/UI/src/components/codeComponents/LogicGateBase.jsx +++ b/UI/src/components/codeComponents/LogicGateBase.jsx @@ -53,13 +53,8 @@ function LogicGateBase({ }; return ( -
- +
+ {/* Handles */} {handleConfigs.map(({ id: handleId, type, position }) => ( From a78018d7d4df585b37a5390aaa26842a4ac0398a Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Tue, 15 Jul 2025 23:52:35 +0300 Subject: [PATCH 053/152] logic gates hitbox fixed, rotation fixed --- UI/assets/circuits-icons.jsx | 120 +++--------------- UI/src/CSS/dnd.css | 17 +-- .../codeComponents/LogicGateBase.jsx | 11 +- UI/src/components/hooks/useRotatedNode.jsx | 4 + UI/src/components/pages/mainPage/settings.jsx | 32 ++--- 5 files changed, 41 insertions(+), 143 deletions(-) diff --git a/UI/assets/circuits-icons.jsx b/UI/assets/circuits-icons.jsx index 1c13cb7b..cdbbc9ca 100644 --- a/UI/assets/circuits-icons.jsx +++ b/UI/assets/circuits-icons.jsx @@ -1,14 +1,7 @@ export const IconAND = ({ SVGClassName, style }) => ( - + - Layer 1 - + AND ( x1="560.32368" fill="none" /> - - ); export const IconOR = ({ SVGClassName, style }) => ( - + - Layer 1 + OR ( ); export const IconNAND = ({ SVGClassName, style }) => ( - + - Layer 1 - + NAND ( x1="610" fill="none" /> - ( ); export const IconNOR = ({ SVGClassName, style }) => ( - + - Layer 1 + NOR ( ); export const IconXOR = ({ SVGClassName, style }) => ( - + - Layer 1 + XOR ( ); export const IconNOT = ({ SVGClassName, style }) => ( - + - Layer 1 + NOT ( ); export const IconInput = ({ SVGClassName, style }) => ( - + - Layer 1 + Input ( ); export const IconOutput = ({ SVGClassName, style }) => ( - + - Layer 1 + Output ( /> -); - -// export const IconSwitchOn = ({ SVGClassName, style }) => ( -// -// -// -// ); -// -// export const IconSwitchOff = ({ SVGClassName }) => ( -// -// -// -// ); -// -// const IconXNOR = ({ SVGClassName }) => ( -// -// -// Layer 1 -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// ); +); \ No newline at end of file diff --git a/UI/src/CSS/dnd.css b/UI/src/CSS/dnd.css index ed1c7fbb..52593b96 100644 --- a/UI/src/CSS/dnd.css +++ b/UI/src/CSS/dnd.css @@ -42,15 +42,13 @@ .logic-gate { display: flex; - box-sizing: border-box; align-items: center; justify-content: center; } .logic-gate-icon { - /*margin: -2% -14% 0 -14%;*/ color: var(--main-0); - object-fit: cover; + /*object-fit: cover;*/ } .switch-icon-wrapper { @@ -67,19 +65,6 @@ height: 70%; } -.logic-gate.input { - width: 60px; - height: 80px; - position: relative; - border: 0.05rem solid var(--main-5); - border-radius: 4px; - background: var(--main-2); - display: flex; - box-sizing: border-box; - align-items: center; - justify-content: center; -} - .input-text { position: absolute; margin-top: 0.3rem; diff --git a/UI/src/components/codeComponents/LogicGateBase.jsx b/UI/src/components/codeComponents/LogicGateBase.jsx index fed39e15..c028af05 100644 --- a/UI/src/components/codeComponents/LogicGateBase.jsx +++ b/UI/src/components/codeComponents/LogicGateBase.jsx @@ -53,13 +53,8 @@ function LogicGateBase({ }; return ( -
- + + {/* Handles */} {handleConfigs.map(({ id: handleId, type, position }) => ( @@ -73,7 +68,7 @@ function LogicGateBase({ connections={handleId.slice(0, 2) === "in" ? 1 : undefined} /> ))} -
+ ); } diff --git a/UI/src/components/hooks/useRotatedNode.jsx b/UI/src/components/hooks/useRotatedNode.jsx index c092fd7b..4fd26e36 100644 --- a/UI/src/components/hooks/useRotatedNode.jsx +++ b/UI/src/components/hooks/useRotatedNode.jsx @@ -74,6 +74,10 @@ export const useRotatedNode = (id, rotation, originalWidth, originalHeight) => { style={{ width: rotatedWidth, height: rotatedHeight, + display: "flex", + alignItems: "center", + justifyContent: "center", + position: "relative", }} >
-
-
-

Kostya switch

-

- Если что, switch есть, он просто закомментирован. Надо ему указать - boolVar и toggleBoolVar -

-
- -
- {/**/} -
-
+ {/*
*/} + {/*
*/} + {/*

Kostya switch

*/} + {/*

*/} + {/* Если что, switch есть, он просто закомментирован. Надо ему указать*/} + {/* boolVar и toggleBoolVar*/} + {/*

*/} + {/*
*/} + + {/*
*/} + {/* */} + {/*
*/} + {/*
*/}
); } From 620c2f967aab352c4295f1b3637940f57fc1bf66 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Wed, 16 Jul 2025 00:21:41 +0300 Subject: [PATCH 054/152] kostya switch added --- UI/src/components/pages/mainPage.jsx | 7 +++ UI/src/components/pages/mainPage/select.jsx | 30 +++++++++++++ UI/src/components/pages/mainPage/settings.jsx | 43 +++++++++++-------- UI/src/components/pages/mainPage/switch.jsx | 15 ------- UI/src/components/utils/loadLocalStorage.js | 2 + UI/src/components/utils/pasteElements.js | 17 ++++++-- 6 files changed, 77 insertions(+), 37 deletions(-) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 93780ca3..4a20f7aa 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -107,6 +107,7 @@ export default function Main() { const [theme, setTheme] = useState("light"); const [toastPosition, setToastPosition] = useState("top-center"); const [logLevel, setLogLevel] = useState(LOG_LEVELS.ERROR); + const [pastePosition, setPastePosition] = useState("cursor") // Хуки React Flow const [nodes, setNodes, onNodesChangeFromHook] = useNodesState([]); @@ -343,6 +344,7 @@ export default function Main() { reactFlowInstance, setNodes, setEdges, + pastePosition, }); setTimeout(recordHistory, 0); }, [clipboard, reactFlowInstance, recordHistory]); @@ -358,6 +360,7 @@ export default function Main() { setCircuitsMenuState, setLogLevel, setToastPosition, + setPastePosition, }); }, []); @@ -373,6 +376,7 @@ export default function Main() { circuitsMenuState, logLevel, toastPosition, + pastePosition, }; localStorage.setItem("userSettings", JSON.stringify(settings)); }, [ @@ -385,6 +389,7 @@ export default function Main() { circuitsMenuState, logLevel, toastPosition, + pastePosition, ]); //Sets current theme to the whole document @@ -661,6 +666,8 @@ export default function Main() { setShowMinimap={setShowMinimap} currentBG={currentBG} setCurrentBG={setCurrentBG} + pastePosition={pastePosition} + setPastePosition={setPastePosition} theme={theme} setTheme={setTheme} toastPosition={toastPosition} diff --git a/UI/src/components/pages/mainPage/select.jsx b/UI/src/components/pages/mainPage/select.jsx index 86f00aeb..4a2274a9 100644 --- a/UI/src/components/pages/mainPage/select.jsx +++ b/UI/src/components/pages/mainPage/select.jsx @@ -160,3 +160,33 @@ export const SelectNotificationsPosition = ({ ); + +export const SelectPastePosition = ({ + pastePosition, + setPastePosition, + }) => ( + + + + + + + + + + + + + + + Near the cursor + Center of the screen + + + + + +); diff --git a/UI/src/components/pages/mainPage/settings.jsx b/UI/src/components/pages/mainPage/settings.jsx index 42c8d8b8..b70e52da 100644 --- a/UI/src/components/pages/mainPage/settings.jsx +++ b/UI/src/components/pages/mainPage/settings.jsx @@ -1,13 +1,14 @@ import { Link } from "react-router-dom"; import UserIcon from "../../../../assets/userIcon.png"; -import { MinimapSwitch, KostyaSwitch } from "./switch.jsx"; +import { MinimapSwitch } from "./switch.jsx"; import { useNotificationsLevel } from "../mainPage.jsx"; import { SelectCanvasBG, SelectLogLevel, SelectNotificationsPosition, SelectTheme, + SelectPastePosition, } from "./select.jsx"; import React, { useState } from "react"; import { @@ -23,11 +24,13 @@ export default function Settings({ setShowMinimap, currentBG, setCurrentBG, + pastePosition, + setPastePosition, theme, setTheme, closeSettings, - setToastPosition, toastPosition, + setToastPosition, currentLogLevel, setLogLevel, }) { @@ -79,6 +82,8 @@ export default function Settings({ setShowMinimap={setShowMinimap} currentBG={currentBG} setCurrentBG={setCurrentBG} + pastePosition={pastePosition} + setPastePosition={setPastePosition} theme={theme} setTheme={setTheme} toastPosition={toastPosition} @@ -101,6 +106,8 @@ function TabContent({ setTheme, toastPosition, setToastPosition, + pastePosition, + setPastePosition, }) { const { logLevel, setLogLevel } = useNotificationsLevel(); @@ -211,22 +218,22 @@ function TabContent({
- {/*
*/} - {/*
*/} - {/*

Kostya switch

*/} - {/*

*/} - {/* Если что, switch есть, он просто закомментирован. Надо ему указать*/} - {/* boolVar и toggleBoolVar*/} - {/*

*/} - {/*
*/} - - {/*
*/} - {/* */} - {/*
*/} - {/*
*/} +
+
+

Paste position

+

+ Changes position of the paste. +

+
+ +
+ +
+
); } diff --git a/UI/src/components/pages/mainPage/switch.jsx b/UI/src/components/pages/mainPage/switch.jsx index 1640c9bc..3779892a 100644 --- a/UI/src/components/pages/mainPage/switch.jsx +++ b/UI/src/components/pages/mainPage/switch.jsx @@ -16,18 +16,3 @@ export function MinimapSwitch({ minimapState, minimapToggle }) {
); } - -export function KostyaSwitch({ boolVar, toggleBootVar }) { - return ( -
- - - -
- ); -} diff --git a/UI/src/components/utils/loadLocalStorage.js b/UI/src/components/utils/loadLocalStorage.js index 383d73d9..7645b16a 100644 --- a/UI/src/components/utils/loadLocalStorage.js +++ b/UI/src/components/utils/loadLocalStorage.js @@ -8,6 +8,7 @@ export function loadLocalStorage({ setCircuitsMenuState, setLogLevel, setToastPosition, + setPastePosition, }) { const saved = localStorage.getItem("userSettings"); if (!saved) return; @@ -31,4 +32,5 @@ export function loadLocalStorage({ setCircuitsMenuState(parsed.circuitsMenuState); if (parsed.logLevel) setLogLevel(parsed.logLevel); if (parsed.toastPosition) setToastPosition(parsed.toastPosition); + if (parsed.pastePosition) setPastePosition(parsed.pastePosition); } diff --git a/UI/src/components/utils/pasteElements.js b/UI/src/components/utils/pasteElements.js index 6408bb15..dfe1007d 100644 --- a/UI/src/components/utils/pasteElements.js +++ b/UI/src/components/utils/pasteElements.js @@ -7,6 +7,7 @@ export function pasteElements({ reactFlowInstance, setNodes, setEdges, + pastePosition, }) { if (!reactFlowInstance) { console.error("React Flow instance not available"); @@ -18,10 +19,18 @@ export function pasteElements({ if (clipboard.nodes.length === 0) return; - const rawPos = reactFlowInstance.screenToFlowPosition({ - x: mousePosition.x, - y: mousePosition.y, - }); + let rawPos; + if (pastePosition === "cursor") { + rawPos = reactFlowInstance.screenToFlowPosition({ + x: mousePosition.x, + y: mousePosition.y, + }); + } else { + rawPos = reactFlowInstance.screenToFlowPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + }); + } const position = calculatePosition(rawPos, clipboard.nodes[0].type); From 945b32ec953d80ca47bdd0a7c3d9350a2182c076 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Wed, 16 Jul 2025 00:28:54 +0300 Subject: [PATCH 055/152] pasteElements tests updated --- .../unit tests/pasteElements.unit.test.js | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/UI/src/components/utils/__tests__/unit tests/pasteElements.unit.test.js b/UI/src/components/utils/__tests__/unit tests/pasteElements.unit.test.js index 875a68ab..eb783aca 100644 --- a/UI/src/components/utils/__tests__/unit tests/pasteElements.unit.test.js +++ b/UI/src/components/utils/__tests__/unit tests/pasteElements.unit.test.js @@ -6,12 +6,11 @@ jest.mock("../../calculatePosition.js"); jest.mock("../../generateId.js"); describe("pasteElements", () => { - const mockSetNodes = jest.fn((fn) => fn([])); - const mockSetEdges = jest.fn((fn) => fn([])); + let mockSetNodes; + let mockSetEdges; const mockReactFlowInstance = { screenToFlowPosition: jest.fn(() => ({ x: 50, y: 50 })), }; - const clipboard = { nodes: [ { @@ -32,15 +31,16 @@ describe("pasteElements", () => { beforeEach(() => { jest.clearAllMocks(); + mockSetNodes = jest.fn((fn) => fn([])); + mockSetEdges = jest.fn((fn) => fn([])); calculatePosition.mockReturnValue({ x: 100, y: 100 }); - generateId .mockReturnValueOnce("node-1") .mockReturnValueOnce("custom-1") .mockReturnValueOnce("edge-1"); }); - it("does nothing if reactFlowInstance is null", () => { + it("logs error and returns if reactFlowInstance is null", () => { const consoleSpy = jest .spyOn(console, "error") .mockImplementation(() => {}); @@ -51,40 +51,48 @@ describe("pasteElements", () => { reactFlowInstance: null, setNodes: mockSetNodes, setEdges: mockSetEdges, + pastePosition: "cursor", }); expect(consoleSpy).toHaveBeenCalledWith( - "React Flow instance not available", + "React Flow instance not available" ); + expect(mockSetNodes).not.toHaveBeenCalled(); + expect(mockSetEdges).not.toHaveBeenCalled(); }); - it("does nothing if clipboard is empty", () => { + it("only clears selection if clipboard is empty", () => { pasteElements({ clipboard: { nodes: [], edges: [] }, mousePosition: { x: 0, y: 0 }, reactFlowInstance: mockReactFlowInstance, setNodes: mockSetNodes, setEdges: mockSetEdges, + pastePosition: "cursor", }); + // сначала очищаем селекцию узлов и рёбер, второй вызов — нет добавления новых expect(mockSetNodes).toHaveBeenCalledTimes(1); expect(mockSetEdges).toHaveBeenCalledTimes(1); }); - it("pastes gates and wires with offset", () => { + it("pastes elements at cursor position", () => { pasteElements({ clipboard, mousePosition: { x: 50, y: 50 }, reactFlowInstance: mockReactFlowInstance, setNodes: mockSetNodes, setEdges: mockSetEdges, + pastePosition: "cursor", }); expect(mockSetNodes).toHaveBeenCalledTimes(2); expect(mockSetEdges).toHaveBeenCalledTimes(2); - expect(generateId).toHaveBeenCalledTimes(3); - expect(calculatePosition).toHaveBeenCalledWith({ x: 50, y: 50 }, "foo"); + expect(calculatePosition).toHaveBeenCalledWith( + { x: 50, y: 50 }, + "foo" + ); const addNodesFn = mockSetNodes.mock.calls[1][0]; const resultNodes = addNodesFn([]); @@ -107,5 +115,39 @@ describe("pasteElements", () => { target: "node-1", }, ]); + + expect(generateId).toHaveBeenCalledTimes(3); + }); + + it("pastes elements at center position when pastePosition !== 'cursor'", () => { + const originalWidth = window.innerWidth; + const originalHeight = window.innerHeight; + Object.defineProperty(window, "innerWidth", { value: 800, configurable: true }); + Object.defineProperty(window, "innerHeight", { value: 600, configurable: true }); + + pasteElements({ + clipboard, + mousePosition: { x: 0, y: 0 }, + reactFlowInstance: mockReactFlowInstance, + setNodes: mockSetNodes, + setEdges: mockSetEdges, + pastePosition: "center", + }); + + expect(mockReactFlowInstance.screenToFlowPosition).toHaveBeenCalledWith({ + x: 800 / 2, + y: 600 / 2, + }); + + expect(mockSetNodes).toHaveBeenCalledTimes(2); + expect(mockSetEdges).toHaveBeenCalledTimes(2); + + expect(calculatePosition).toHaveBeenCalledWith( + { x: 50, y: 50 }, + "foo" + ); + + Object.defineProperty(window, "innerWidth", { value: originalWidth }); + Object.defineProperty(window, "innerHeight", { value: originalHeight }); }); }); From 9990628d1ae244821db7cb8f84c24717046c6266 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 21:29:20 +0000 Subject: [PATCH 056/152] Automated formatting --- UI/assets/circuits-icons.jsx | 2 +- UI/src/components/pages/mainPage.jsx | 2 +- UI/src/components/pages/mainPage/select.jsx | 10 ++------- .../unit tests/pasteElements.unit.test.js | 22 +++++++++---------- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/UI/assets/circuits-icons.jsx b/UI/assets/circuits-icons.jsx index cdbbc9ca..31b3941a 100644 --- a/UI/assets/circuits-icons.jsx +++ b/UI/assets/circuits-icons.jsx @@ -458,4 +458,4 @@ export const IconOutput = ({ SVGClassName, style }) => ( /> -); \ No newline at end of file +); diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 4a20f7aa..b913318f 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -107,7 +107,7 @@ export default function Main() { const [theme, setTheme] = useState("light"); const [toastPosition, setToastPosition] = useState("top-center"); const [logLevel, setLogLevel] = useState(LOG_LEVELS.ERROR); - const [pastePosition, setPastePosition] = useState("cursor") + const [pastePosition, setPastePosition] = useState("cursor"); // Хуки React Flow const [nodes, setNodes, onNodesChangeFromHook] = useNodesState([]); diff --git a/UI/src/components/pages/mainPage/select.jsx b/UI/src/components/pages/mainPage/select.jsx index 4a2274a9..989a0393 100644 --- a/UI/src/components/pages/mainPage/select.jsx +++ b/UI/src/components/pages/mainPage/select.jsx @@ -161,15 +161,9 @@ export const SelectNotificationsPosition = ({ ); -export const SelectPastePosition = ({ - pastePosition, - setPastePosition, - }) => ( +export const SelectPastePosition = ({ pastePosition, setPastePosition }) => ( - + diff --git a/UI/src/components/utils/__tests__/unit tests/pasteElements.unit.test.js b/UI/src/components/utils/__tests__/unit tests/pasteElements.unit.test.js index eb783aca..0c8f4a16 100644 --- a/UI/src/components/utils/__tests__/unit tests/pasteElements.unit.test.js +++ b/UI/src/components/utils/__tests__/unit tests/pasteElements.unit.test.js @@ -55,7 +55,7 @@ describe("pasteElements", () => { }); expect(consoleSpy).toHaveBeenCalledWith( - "React Flow instance not available" + "React Flow instance not available", ); expect(mockSetNodes).not.toHaveBeenCalled(); expect(mockSetEdges).not.toHaveBeenCalled(); @@ -89,10 +89,7 @@ describe("pasteElements", () => { expect(mockSetNodes).toHaveBeenCalledTimes(2); expect(mockSetEdges).toHaveBeenCalledTimes(2); - expect(calculatePosition).toHaveBeenCalledWith( - { x: 50, y: 50 }, - "foo" - ); + expect(calculatePosition).toHaveBeenCalledWith({ x: 50, y: 50 }, "foo"); const addNodesFn = mockSetNodes.mock.calls[1][0]; const resultNodes = addNodesFn([]); @@ -122,8 +119,14 @@ describe("pasteElements", () => { it("pastes elements at center position when pastePosition !== 'cursor'", () => { const originalWidth = window.innerWidth; const originalHeight = window.innerHeight; - Object.defineProperty(window, "innerWidth", { value: 800, configurable: true }); - Object.defineProperty(window, "innerHeight", { value: 600, configurable: true }); + Object.defineProperty(window, "innerWidth", { + value: 800, + configurable: true, + }); + Object.defineProperty(window, "innerHeight", { + value: 600, + configurable: true, + }); pasteElements({ clipboard, @@ -142,10 +145,7 @@ describe("pasteElements", () => { expect(mockSetNodes).toHaveBeenCalledTimes(2); expect(mockSetEdges).toHaveBeenCalledTimes(2); - expect(calculatePosition).toHaveBeenCalledWith( - { x: 50, y: 50 }, - "foo" - ); + expect(calculatePosition).toHaveBeenCalledWith({ x: 50, y: 50 }, "foo"); Object.defineProperty(window, "innerWidth", { value: originalWidth }); Object.defineProperty(window, "innerHeight", { value: originalHeight }); From 74db37b62ca1c5a5100202eb44249ef4d1ce1b18 Mon Sep 17 00:00:00 2001 From: doshq Date: Wed, 16 Jul 2025 15:59:26 +0300 Subject: [PATCH 057/152] Intermediate stage --- .../pages/mainPage/runnerHandler.jsx | 2 +- backend/profile/__init__.py | 0 backend/profile/config.py | 15 ++++++ backend/profile/db.py | 6 +++ backend/profile/main.py | 16 ++++++ backend/profile/models.py | 13 +++++ backend/profile/routers/__init__.py | 0 backend/profile/routers/profile.py | 32 ++++++++++++ backend/profile/routers/project.py | 50 +++++++++++++++++++ backend/profile/schemas.py | 25 ++++++++++ backend/profile/verify.py | 24 +++++++++ backend/tests/__init__.py | 0 backend/tests/profile_tests/__init__.py | 0 backend/tests/profile_tests/conftest.py | 31 ++++++++++++ backend/tests/profile_tests/test_get_info.py | 30 +++++++++++ 15 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 backend/profile/__init__.py create mode 100644 backend/profile/config.py create mode 100644 backend/profile/db.py create mode 100644 backend/profile/main.py create mode 100644 backend/profile/models.py create mode 100644 backend/profile/routers/__init__.py create mode 100644 backend/profile/routers/profile.py create mode 100644 backend/profile/routers/project.py create mode 100644 backend/profile/schemas.py create mode 100644 backend/profile/verify.py create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/profile_tests/__init__.py create mode 100644 backend/tests/profile_tests/conftest.py create mode 100644 backend/tests/profile_tests/test_get_info.py diff --git a/UI/src/components/pages/mainPage/runnerHandler.jsx b/UI/src/components/pages/mainPage/runnerHandler.jsx index 744aff88..7854bf73 100644 --- a/UI/src/components/pages/mainPage/runnerHandler.jsx +++ b/UI/src/components/pages/mainPage/runnerHandler.jsx @@ -62,7 +62,7 @@ export const handleSimulateClick = ({ // Initialize socket connection if (!socketRef.current) { - socketRef.current = io("/", { + socketRef.current = io("http://localhost:80", { transports: ["websocket"], path: "/socket.io", }); diff --git a/backend/profile/__init__.py b/backend/profile/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/profile/config.py b/backend/profile/config.py new file mode 100644 index 00000000..4bc3856a --- /dev/null +++ b/backend/profile/config.py @@ -0,0 +1,15 @@ +import os +from pathlib import Path +from dotenv import load_dotenv + + +dotenv_path = Path('.env') +load_dotenv() + +MONGO_URI = os.getenv("MONGO_URI") +SECRET = os.getenv("SECRET") + +if not MONGO_URI: + raise ValueError("MONGO_URI not set in environment variables") +if not SECRET: + raise ValueError("SECRET not set in environment variables") \ No newline at end of file diff --git a/backend/profile/db.py b/backend/profile/db.py new file mode 100644 index 00000000..4e5c755e --- /dev/null +++ b/backend/profile/db.py @@ -0,0 +1,6 @@ +from backend.profile.config import MONGO_URI +from motor.motor_asyncio import AsyncIOMotorClient + +client = AsyncIOMotorClient(MONGO_URI) +db = client["visual-circuit-designer"] +user_collection = db["Projects"] diff --git a/backend/profile/main.py b/backend/profile/main.py new file mode 100644 index 00000000..522c3196 --- /dev/null +++ b/backend/profile/main.py @@ -0,0 +1,16 @@ +import warnings +from fastapi import FastAPI + +from backend.profile.routers import profile, project +try: + from backend.profile.verify import temp_router +except (ModuleNotFoundError, ImportError): + # warnings.warn("Verify handler not found, skipping...") + temp_router = None + + +app = FastAPI() +app.include_router(profile.router) +app.include_router(project.router) +if temp_router: + app.include_router(temp_router, prefix="/auth") diff --git a/backend/profile/models.py b/backend/profile/models.py new file mode 100644 index 00000000..ec0c2eab --- /dev/null +++ b/backend/profile/models.py @@ -0,0 +1,13 @@ +from uuid import UUID +from pydantic import BaseModel, EmailStr + + +class UserDB(BaseModel): + id: UUID + name: str + username: str + email: EmailStr + hashed_password: str + is_active: bool = True + is_superuser: bool = False + is_verified: bool = False \ No newline at end of file diff --git a/backend/profile/routers/__init__.py b/backend/profile/routers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/profile/routers/profile.py b/backend/profile/routers/profile.py new file mode 100644 index 00000000..4949c39d --- /dev/null +++ b/backend/profile/routers/profile.py @@ -0,0 +1,32 @@ +from fastapi import APIRouter, HTTPException +from backend.profile.db import db +from backend.profile.schemas import UserProfile, UpdateName, UpdateEmail, UpdatePassword + +router = APIRouter(prefix="/api/profile", tags=["profile"]) + +@router.get("/{id}") +async def get_profile(id: str): + user = await db.users.find_one({"_id": id}) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + +@router.patch("/{id}/name") +async def update_name(id: str, body: UpdateName): + await db.users.update_one({"_id": id}, {"$set": {"name": body.name}}) + return {"status": "name updated"} + +@router.patch("/{id}/email") +async def update_email(id: str, body: UpdateEmail): + await db.users.update_one({"_id": id}, {"$set": {"email": body.email}}) + return {"status": "email updated"} + +@router.patch("/{id}/password") +async def update_password(id: str, body: UpdatePassword): + # если используешь fastapi-users, здесь нужно через UserManager менять пароль + return {"status": "password updated"} + +@router.delete("/{id}") +async def delete_profile(id: str): + await db.users.delete_one({"_id": id}) + return {"status": "deleted"} \ No newline at end of file diff --git a/backend/profile/routers/project.py b/backend/profile/routers/project.py new file mode 100644 index 00000000..7291d79e --- /dev/null +++ b/backend/profile/routers/project.py @@ -0,0 +1,50 @@ +from fastapi import APIRouter +from backend.profile.db import db +from backend.profile.schemas import Project + +router = APIRouter(prefix="/api/profile/{id}/project", tags=["projects"]) + +@router.post("") +async def create_project(id: str, project: Project): + await db.projects.insert_one({**project.model_dump(), "owner_id": id}) + return {"status": "project created"} + +@router.get("") +async def get_all_projects(id: str): + projects = await db.projects.find({"owner_id": id}).to_list(100) + return projects + +@router.get("/{pid}") +async def get_project(id: str, pid: str): + project = await db.projects.find_one({"owner_id": id, "pid": pid}) + return project + +@router.get("/{pid}/name") +async def get_project_name(id: str, pid: str): + proj = await db.projects.find_one({"owner_id": id, "pid": pid}, {"name": 1}) + return {"name": proj["name"]} + +@router.get("/{pid}/date-created") +async def get_project_date(id: str, pid: str): + proj = await db.projects.find_one({"owner_id": id, "pid": pid}, {"date_created": 1}) + return {"date_created": proj["date_created"]} + +@router.patch("/{pid}/name") +async def update_project_name(id: str, pid: str, data: dict): + await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"name": data["name"]}}) + return {"status": "name updated"} + +@router.patch("/{pid}/circuit") +async def update_project_circuit(id: str, pid: str, data: dict): + await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"circuit": data}}) + return {"status": "circuit updated"} + +@router.patch("/{pid}/verilog") +async def update_project_verilog(id: str, pid: str, data: dict): + await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"verilog": data["verilog"]}}) + return {"status": "verilog updated"} + +@router.delete("/{pid}") +async def delete_project(id: str, pid: str): + await db.projects.delete_one({"owner_id": id, "pid": pid}) + return {"status": "project deleted"} \ No newline at end of file diff --git a/backend/profile/schemas.py b/backend/profile/schemas.py new file mode 100644 index 00000000..473a4c84 --- /dev/null +++ b/backend/profile/schemas.py @@ -0,0 +1,25 @@ +from pydantic import EmailStr +from typing import Optional +from uuid import UUID +from fastapi_users import schemas + +class UserProfile(schemas.BaseModel): + id: UUID + name: str + email: EmailStr + +class UpdateName(schemas.BaseModel): + name: str + +class UpdateEmail(schemas.BaseModel): + email: EmailStr + +class UpdatePassword(schemas.BaseModel): + password: str + +class Project(schemas.BaseModel): + pid: UUID + name: str + date_created: str + circuit: dict + verilog: str \ No newline at end of file diff --git a/backend/profile/verify.py b/backend/profile/verify.py new file mode 100644 index 00000000..6f11f31d --- /dev/null +++ b/backend/profile/verify.py @@ -0,0 +1,24 @@ +# from uuid import UUID +# +# from fastapi import APIRouter, Depends +# from fastapi_users import FastAPIUsers +# +# from backend.profile.models import UserDB +# +# temp_router = APIRouter() +# +# fastapi_users = FastAPIUsers[UserDB, UUID]( +# get_user_manager, +# [auth_backend, refresh_backend], +# ) +# +# get_current_user = fastapi_users.current_user(active=False, verified=False) +# +# @temp_router.get("/verify", tags=["auth"]) +# async def verify_access_token(user: UserDB = Depends(get_current_user)): +# return { +# "valid": True, +# "user_id": str(user.id), +# "email": user.email, +# } +# diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/profile_tests/__init__.py b/backend/tests/profile_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/profile_tests/conftest.py b/backend/tests/profile_tests/conftest.py new file mode 100644 index 00000000..229c8176 --- /dev/null +++ b/backend/tests/profile_tests/conftest.py @@ -0,0 +1,31 @@ +import httpx +import pytest_asyncio + +from backend.profile.db import user_collection +from backend.profile.main import app + + +@pytest_asyncio.fixture +async def test_client(): + transport = httpx.ASGITransport(app=app) + return httpx.AsyncClient(transport=transport, base_url="http://test") + +@pytest_asyncio.fixture(autouse=True) +async def cleanup(test_client): + await user_collection.delete_many({}) + yield + await user_collection.delete_many({}) + await test_client.aclose() + +@pytest_asyncio.fixture +async def registered_user(test_client): + user_data = { + "name": "Test User", + "username": "testuser", + "email": "test@example.com", + "password": "TestPassword123" + } + response = await test_client.post("/auth/register", json=user_data) + assert response.status_code == 201 + + return user_data, response.json() \ No newline at end of file diff --git a/backend/tests/profile_tests/test_get_info.py b/backend/tests/profile_tests/test_get_info.py new file mode 100644 index 00000000..53109d68 --- /dev/null +++ b/backend/tests/profile_tests/test_get_info.py @@ -0,0 +1,30 @@ +import pytest +from fastapi import status + +from backend.profile.main import app + +@pytest.mark.asyncio +async def test_get_info(test_client, registered_user): + user_data, user = registered_user + + print("Registered routes:") + for route in app.routes: + if hasattr(route, "path"): + print(f"{route.path}") + + login_response = await test_client.post( + "/auth/login", + data={ + "username": user_data["email"], + "password": user_data["password"] + } + ) + token = login_response.json().get("access_token") + assert login_response.status_code == status.HTTP_200_OK + + response = await test_client.get( + "/api/profile", + headers={"Authorization": f"Bearer {token}"} + ) + + print(response.json()) \ No newline at end of file From e17e12f5c8ac273607bf00718560b0a1c45be2e9 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Wed, 16 Jul 2025 20:58:31 +0300 Subject: [PATCH 058/152] context menu standardized --- UI/src/CSS/tabs.css | 12 +---- UI/src/components/pages/mainPage.jsx | 45 ++++++++++++++++--- UI/src/components/pages/mainPage/tabs.jsx | 19 +++----- .../utils/calculateContextMenuPosition.js | 5 --- 4 files changed, 46 insertions(+), 35 deletions(-) diff --git a/UI/src/CSS/tabs.css b/UI/src/CSS/tabs.css index d62c4d94..3b6a11f8 100644 --- a/UI/src/CSS/tabs.css +++ b/UI/src/CSS/tabs.css @@ -124,18 +124,8 @@ } /* Context Menu Styles */ -.context-menu { - position: fixed; - background: var(--menu-lighter); - border: 1px solid var(--main-1); - border-radius: 4px; - padding: 4px 0; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); - z-index: 1000; - min-width: 120px; -} - .context-menu-item { + border-radius: 0.5rem; padding: 8px 16px; cursor: pointer; color: var(--main-0); diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index b913318f..298b8323 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -425,16 +425,40 @@ export default function Main() { const onNodeContextMenu = useCallback((event, node) => { event.preventDefault(); const pane = ref.current.getBoundingClientRect(); - setMenu(calculateContextMenuPosition(event, node, pane, "node")); + const menuPosition = calculateContextMenuPosition(event, pane); + setMenu({ + id: node.id, + name: node.type, + type: "node", + top: menuPosition.top, + left: menuPosition.left, + right: menuPosition.right, + bottom: menuPosition.bottom, + }); }, []); const onEdgeContextMenu = useCallback((event, edge) => { event.preventDefault(); const pane = ref.current.getBoundingClientRect(); - setMenu(calculateContextMenuPosition(event, edge, pane, "edge")); + const menuPosition = calculateContextMenuPosition(event, pane); + setMenu({ + id: edge.id, + name: edge.type, + type: "edge", + position: menuPosition, + top: menuPosition.top, + left: menuPosition.left, + right: menuPosition.right, + bottom: menuPosition.bottom, + }); }, []); - const onPaneClick = useCallback(() => setMenu(null), []); + const onPaneContextMenu = useCallback((event, edge) => { + event.preventDefault(); + const pane = ref.current.getBoundingClientRect(); + const menuPosition = calculateContextMenuPosition(event, pane); + setMenu(calculateContextMenuPosition(event, edge, pane, "pane")); + }, []); //Allows user to download circuit JSON const saveCircuit = () => saveCircuitUtil(nodes, edges); @@ -547,9 +571,9 @@ export default function Main() { defaultEdgeOptions={{ type: activeWire, }} + onPaneContextMenu={onPaneContextMenu} onNodeContextMenu={onNodeContextMenu} onEdgeContextMenu={onEdgeContextMenu} - onPaneClick={onPaneClick} onConnect={onConnect} onNodeDragStop={onNodeDragStop} onDrop={onDrop} @@ -600,11 +624,11 @@ export default function Main() { {menu && menu.type === "node" && ( - + )} {menu && menu.type === "edge" && ( - + )} { - setMenu(null); setOpenSettings(false); }} /> + +
{ + setMenu(null); + }} + /> + el.removeEventListener("wheel", onWheel); }, []); - // Close context menu on click outside - useEffect(() => { - const handleClickOutside = (e) => { - if (contextMenu && !e.target.closest(".context-menu")) { - setContextMenu(null); - } - }; - - document.addEventListener("click", handleClickOutside); - return () => document.removeEventListener("click", handleClickOutside); - }, [contextMenu]); - // Focus textarea when editing starts useEffect(() => { if (editingTabId && textareaRefs.current[editingTabId]) { @@ -194,6 +182,13 @@ export default function TabsContainer({ )}
)} + +
{ + setContextMenu(null); + }} + />
); } diff --git a/UI/src/components/utils/calculateContextMenuPosition.js b/UI/src/components/utils/calculateContextMenuPosition.js index 6125d56a..10434788 100644 --- a/UI/src/components/utils/calculateContextMenuPosition.js +++ b/UI/src/components/utils/calculateContextMenuPosition.js @@ -1,13 +1,8 @@ export function calculateContextMenuPosition( event, - object, containerRect, - string, ) { return { - id: object.id, - name: object.type, - type: string, top: event.clientY < containerRect.height - 200 && event.clientY, left: event.clientX < containerRect.width - 200 && event.clientX, right: From c2f58aea86022815fee0cc05c9571d4956d1eba0 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Wed, 16 Jul 2025 23:34:29 +0300 Subject: [PATCH 059/152] context menu position fixed, wire type color fixed --- UI/src/CSS/select.css | 4 ++-- UI/src/components/pages/mainPage.jsx | 1 + UI/src/components/pages/mainPage/tabs.jsx | 15 +++++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/UI/src/CSS/select.css b/UI/src/CSS/select.css index 8a6ce1bf..a380dbdf 100644 --- a/UI/src/CSS/select.css +++ b/UI/src/CSS/select.css @@ -108,7 +108,7 @@ button { padding: 0.5rem; margin: 0.5rem; border-radius: 0.35rem; - background: var(--main-2); + background: var(--menu-lighter); border: 1px solid var(--main-4); color: var(--main-0); width: 75%; @@ -116,7 +116,7 @@ button { } .SelectTriggerWire:hover { - background-color: var(--select-1); + background-color: var(--main-4); } .SelectTriggerWire[data-placeholder] { diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 298b8323..96fd1a8d 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -557,6 +557,7 @@ export default function Main() { activeTabId={activeTabId} onTabsChange={setTabs} onActiveTabIdChange={handleTabSwitch} + ref={ref} />
diff --git a/UI/src/components/pages/mainPage/tabs.jsx b/UI/src/components/pages/mainPage/tabs.jsx index 57c5b03c..c7b9883b 100644 --- a/UI/src/components/pages/mainPage/tabs.jsx +++ b/UI/src/components/pages/mainPage/tabs.jsx @@ -1,12 +1,14 @@ import React, { useRef, useEffect, useState } from "react"; import { IconCloseCross } from "../../../../assets/ui-icons.jsx"; import { initializeTabHistory } from "../../utils/initializeTabHistory.js"; +import { calculateContextMenuPosition} from "../../utils/calculateContextMenuPosition.js"; export default function TabsContainer({ tabs, activeTabId, onTabsChange, onActiveTabIdChange, + ref, }) { const scrollRef = useRef(null); const textareaRefs = useRef({}); @@ -71,10 +73,13 @@ export default function TabsContainer({ const handleContextMenu = (e, tabId) => { e.preventDefault(); + const menuPosition = calculateContextMenuPosition(e, ref.current.getBoundingClientRect()); setContextMenu({ - x: e.clientX, - y: e.clientY, tabId: tabId, + top: menuPosition.top, + left: menuPosition.left, + right: menuPosition.right, + bottom: menuPosition.bottom, }); }; @@ -162,8 +167,10 @@ export default function TabsContainer({
Date: Wed, 16 Jul 2025 23:46:49 +0300 Subject: [PATCH 060/152] Fix docker caching and connect to postgres --- auth/Dockerfile | 21 +++++++++++---------- auth/default.nix | 1 - auth/src/app.cpp | 33 +++++++++++++++++++++++++++++---- docker-compose.yml | 25 +++++++++++++++++++++++-- nginx.conf | 10 ++++++++-- postgres-init/users.sql | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 postgres-init/users.sql diff --git a/auth/Dockerfile b/auth/Dockerfile index 3c59d63a..69ab5e8b 100644 --- a/auth/Dockerfile +++ b/auth/Dockerfile @@ -1,20 +1,21 @@ FROM nixos/nix:latest AS build -WORKDIR /app -COPY CMakeLists.txt . -COPY src/ src -COPY default.nix . +WORKDIR /build +ADD ./default.nix /build +RUN nix-shell --run exit -RUN --mount=type=cache,target=/tmp/nix-store cp -R /tmp/nix-store/nix /nix +ADD ./src/ /build/src +ADD ./CMakeLists.txt /build RUN nix-build -RUN --mount=type=cache,target=/tmp/nix-store cp -R /nix /tmp/nix-store/nix -RUN mkdir nix-deps -RUN cp -R $(nix-store -qR result/) nix-deps + +RUN mkdir /build/nix-deps +RUN cp -R $(nix-store -qR /build/result/) /build/nix-deps FROM scratch WORKDIR /app -COPY --from=build /app/nix-deps /nix/store -COPY --from=build /app/result /app +COPY --from=build /build/nix-deps /nix/store +COPY --from=build /build/result /app + ENTRYPOINT [ "/app/bin/VCDAuth" ] \ No newline at end of file diff --git a/auth/default.nix b/auth/default.nix index 2b5a8e8e..963f8cbc 100644 --- a/auth/default.nix +++ b/auth/default.nix @@ -1,7 +1,6 @@ with import {}; let - # Переопределяем Poco с поддержкой PostgreSQL pocoWithPostgres = pkgs.poco.overrideAttrs (oldAttrs: { buildInputs = (oldAttrs.buildInputs or []) ++ [ pkgs.postgresql ]; cmakeFlags = (oldAttrs.cmakeFlags or []) ++ [ diff --git a/auth/src/app.cpp b/auth/src/app.cpp index 1e119ef3..0476ff7e 100644 --- a/auth/src/app.cpp +++ b/auth/src/app.cpp @@ -6,18 +6,22 @@ #include #include +#include #include #include #include #include #include +#include #include #include +#include using Poco::Data::Session; using Poco::Net::HTTPServer; using Poco::Net::HTTPServerParams; using Poco::Net::ServerSocket; +using Poco::Environment; void AppAuthServer::initialize(Application& self) { loadConfiguration(); @@ -25,19 +29,40 @@ void AppAuthServer::initialize(Application& self) { } int AppAuthServer::main(std::vector< std::string > const& args) { + char connectionString[256]; + Poco::UInt16 port; + std::string db_host; + Poco::UInt16 db_port; + std::string db_user; + std::string db_password; + std::string db_name; + + try { + db_host = config().getString("PostgreSQL.host"); + db_port = config().getUInt16("PostgreSQL.port"); + db_user = config().getString("PostgreSQL.user"); + db_password = config().getString("PostgreSQL.password"); + db_name = config().getString("PostgreSQL.db"); + } catch (Poco::NotFoundException& e) { + db_host = Environment::get("POSTGRES_HOST"); + db_port = std::stoi(Environment::get("POSTGRES_PORT")); + db_user = Environment::get("POSTGRES_USER"); + db_password = Environment::get("POSTGRES_PASSWORD"); + db_name = Environment::get("POSTGRES_DB"); + } + snprintf(connectionString, 256, "host=%s port=%hu user=%s password=%s dbname=%s", db_host.c_str(), db_port, db_user.c_str(), db_password.c_str(), db_name.c_str()); + Poco::Data::PostgreSQL::Connector::registerConnector(); Session dbSession( "PostgreSQL", - "host=localhost port=5432 user=VCD password=12345 dbname=VCD" + connectionString ); DBConnector db(dbSession); TokenManager tokenManager; - Poco::UInt16 port = config().getUInt16("HTTP.port"); - - ServerSocket socket(port); + ServerSocket socket(8080); HTTPServer httpServer( new AuthRequestHandlerFactory(db, tokenManager), socket, diff --git a/docker-compose.yml b/docker-compose.yml index 48e8c609..fca9675f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,28 @@ services: + postgres: + image: postgres:17-alpine + environment: + POSTGRES_DB: "vcd" + POSTGRES_USER: "vcd" + POSTGRES_PASSWORD: "pgpwd4vcd" + volumes: + - ./postgres-init:/docker-entrypoint-initdb.d + runner: + build: ./RunnerNode + auth: + build: ./auth + environment: + POSTGRES_DB: "vcd" + POSTGRES_USER: "vcd" + POSTGRES_PASSWORD: "pgpwd4vcd" + POSTGRES_HOST: "postgres" + POSTGRES_PORT: 5432 + depends_on: + - postgres webserver: build: ./UI volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro - runner: - build: ./RunnerNode \ No newline at end of file + depends_on: + - runner + - auth \ No newline at end of file diff --git a/nginx.conf b/nginx.conf index 026669a0..8415b0f9 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,6 +1,6 @@ server { - listen 80; - server_name localhost; + listen 80 default_server; + server_name _; location /socket.io { proxy_set_header X-Real-IP $remote_addr; @@ -16,6 +16,12 @@ server { proxy_set_header Connection "upgrade"; } + location /api/auth { + proxy_pass http://auth:8080; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + } + location / { root /usr/share/nginx/html; try_files $uri /index.html; diff --git a/postgres-init/users.sql b/postgres-init/users.sql new file mode 100644 index 00000000..b4073315 --- /dev/null +++ b/postgres-init/users.sql @@ -0,0 +1,35 @@ +CREATE TABLE public.users ( + id integer NOT NULL, + username character varying(50) NOT NULL, + email character varying(100) NOT NULL, + name character varying(50) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + password_hash text NOT NULL, + salt character varying(64) NOT NULL +); + + +ALTER TABLE public.users OWNER TO vcd; + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.users_id_seq OWNER TO vcd; + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_email_key UNIQUE (email); + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_username_key UNIQUE (username); \ No newline at end of file From e03be23d19e5a0cde5278237982689cf4f98c1fd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 20:47:09 +0000 Subject: [PATCH 061/152] Automated formatting --- auth/src/app.cpp | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/auth/src/app.cpp b/auth/src/app.cpp index 0476ff7e..871f7ac8 100644 --- a/auth/src/app.cpp +++ b/auth/src/app.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -15,13 +16,12 @@ #include #include #include -#include +using Poco::Environment; using Poco::Data::Session; using Poco::Net::HTTPServer; using Poco::Net::HTTPServerParams; using Poco::Net::ServerSocket; -using Poco::Environment; void AppAuthServer::initialize(Application& self) { loadConfiguration(); @@ -29,35 +29,41 @@ void AppAuthServer::initialize(Application& self) { } int AppAuthServer::main(std::vector< std::string > const& args) { - char connectionString[256]; + char connectionString[256]; Poco::UInt16 port; - std::string db_host; + std::string db_host; Poco::UInt16 db_port; - std::string db_user; - std::string db_password; - std::string db_name; + std::string db_user; + std::string db_password; + std::string db_name; try { - db_host = config().getString("PostgreSQL.host"); - db_port = config().getUInt16("PostgreSQL.port"); - db_user = config().getString("PostgreSQL.user"); + db_host = config().getString("PostgreSQL.host"); + db_port = config().getUInt16("PostgreSQL.port"); + db_user = config().getString("PostgreSQL.user"); db_password = config().getString("PostgreSQL.password"); - db_name = config().getString("PostgreSQL.db"); + db_name = config().getString("PostgreSQL.db"); } catch (Poco::NotFoundException& e) { - db_host = Environment::get("POSTGRES_HOST"); - db_port = std::stoi(Environment::get("POSTGRES_PORT")); - db_user = Environment::get("POSTGRES_USER"); + db_host = Environment::get("POSTGRES_HOST"); + db_port = std::stoi(Environment::get("POSTGRES_PORT")); + db_user = Environment::get("POSTGRES_USER"); db_password = Environment::get("POSTGRES_PASSWORD"); - db_name = Environment::get("POSTGRES_DB"); + db_name = Environment::get("POSTGRES_DB"); } - snprintf(connectionString, 256, "host=%s port=%hu user=%s password=%s dbname=%s", db_host.c_str(), db_port, db_user.c_str(), db_password.c_str(), db_name.c_str()); + snprintf( + connectionString, + 256, + "host=%s port=%hu user=%s password=%s dbname=%s", + db_host.c_str(), + db_port, + db_user.c_str(), + db_password.c_str(), + db_name.c_str() + ); Poco::Data::PostgreSQL::Connector::registerConnector(); - Session dbSession( - "PostgreSQL", - connectionString - ); + Session dbSession("PostgreSQL", connectionString); DBConnector db(dbSession); TokenManager tokenManager; From 43068b3e2c53fe117815c658d0056ff5a50a326f Mon Sep 17 00:00:00 2001 From: doshq Date: Thu, 17 Jul 2025 15:33:57 +0300 Subject: [PATCH 062/152] Add update handlers --- backend/{ => auth}/__init__.py | 0 backend/{app => auth}/config.py | 0 backend/{app => auth}/db.py | 5 +- backend/{app => auth}/main.py | 12 ++-- backend/{app => auth}/models.py | 1 - backend/{app => auth}/schema.py | 0 backend/{app => auth/users}/__init__.py | 0 backend/{app => auth}/users/manager.py | 7 +-- backend/{app => auth}/users/mongo_users.py | 21 +++---- .../users => profile/database}/__init__.py | 0 backend/profile/{ => database}/db.py | 0 backend/profile/database/mongo_users.py | 60 +++++++++++++++++++ backend/profile/models.py | 3 +- backend/profile/routers/profile.py | 40 ++++++++++--- backend/profile/utils.py | 16 +++++ backend/tests/{ => auth_test}/__init__.py | 0 backend/tests/{ => auth_test}/conftest.py | 4 +- .../tests/{ => auth_test}/test_delete_user.py | 0 .../{ => auth_test}/test_get_curr_user.py | 0 backend/tests/{ => auth_test}/test_login.py | 2 +- .../test_login_wrong_password.py | 0 .../test_password_validation.py | 0 .../tests/{ => auth_test}/test_protection.py | 0 .../tests/{ => auth_test}/test_register.py | 0 .../test_register_invalid_email.py | 0 .../test_registered_duplicate_email.py | 0 .../test_registered_duplicate_username.py | 0 .../tests/{ => auth_test}/test_update_user.py | 0 .../{ => auth_test}/test_verify_token.py | 0 backend/tests/profile_tests/conftest.py | 22 ++++--- backend/tests/profile_tests/test_get_info.py | 27 +++++---- .../tests/profile_tests/test_update_email.py | 42 +++++++++++++ 32 files changed, 202 insertions(+), 60 deletions(-) rename backend/{ => auth}/__init__.py (100%) rename backend/{app => auth}/config.py (100%) rename backend/{app => auth}/db.py (51%) rename backend/{app => auth}/main.py (88%) rename backend/{app => auth}/models.py (89%) rename backend/{app => auth}/schema.py (100%) rename backend/{app => auth/users}/__init__.py (100%) rename backend/{app => auth}/users/manager.py (95%) rename backend/{app => auth}/users/mongo_users.py (80%) rename backend/{app/users => profile/database}/__init__.py (100%) rename backend/profile/{ => database}/db.py (100%) create mode 100644 backend/profile/database/mongo_users.py create mode 100644 backend/profile/utils.py rename backend/tests/{ => auth_test}/__init__.py (100%) rename backend/tests/{ => auth_test}/conftest.py (91%) rename backend/tests/{ => auth_test}/test_delete_user.py (100%) rename backend/tests/{ => auth_test}/test_get_curr_user.py (100%) rename backend/tests/{ => auth_test}/test_login.py (96%) rename backend/tests/{ => auth_test}/test_login_wrong_password.py (100%) rename backend/tests/{ => auth_test}/test_password_validation.py (100%) rename backend/tests/{ => auth_test}/test_protection.py (100%) rename backend/tests/{ => auth_test}/test_register.py (100%) rename backend/tests/{ => auth_test}/test_register_invalid_email.py (100%) rename backend/tests/{ => auth_test}/test_registered_duplicate_email.py (100%) rename backend/tests/{ => auth_test}/test_registered_duplicate_username.py (100%) rename backend/tests/{ => auth_test}/test_update_user.py (100%) rename backend/tests/{ => auth_test}/test_verify_token.py (100%) create mode 100644 backend/tests/profile_tests/test_update_email.py diff --git a/backend/__init__.py b/backend/auth/__init__.py similarity index 100% rename from backend/__init__.py rename to backend/auth/__init__.py diff --git a/backend/app/config.py b/backend/auth/config.py similarity index 100% rename from backend/app/config.py rename to backend/auth/config.py diff --git a/backend/app/db.py b/backend/auth/db.py similarity index 51% rename from backend/app/db.py rename to backend/auth/db.py index ce5b67f5..bee9f572 100644 --- a/backend/app/db.py +++ b/backend/auth/db.py @@ -1,6 +1,9 @@ -from backend.app.config import MONGO_URI +from backend.auth.config import MONGO_URI from motor.motor_asyncio import AsyncIOMotorClient +from backend.auth.users.mongo_users import MongoUserDatabase client = AsyncIOMotorClient(MONGO_URI) db = client["visual-circuit-designer"] user_collection = db["Users"] + +user_db = MongoUserDatabase(user_collection) diff --git a/backend/app/main.py b/backend/auth/main.py similarity index 88% rename from backend/app/main.py rename to backend/auth/main.py index 7fd1e98c..979534b3 100644 --- a/backend/app/main.py +++ b/backend/auth/main.py @@ -9,11 +9,11 @@ from pymongo.errors import DuplicateKeyError from starlette.responses import JSONResponse -from ..app.schema import UserCreate, UserRead, UserUpdate -from ..app.models import UserDB -from ..app.db import user_collection -from ..app.users.mongo_users import MongoUserDatabase -from ..app.users.manager import MyUserManager, auth_backend, refresh_backend +from ..auth.schema import UserCreate, UserRead, UserUpdate +from ..auth.models import UserDB +from ..auth.db import user_collection +from ..auth.users.mongo_users import MongoUserDatabase +from ..auth.users.manager import MyUserManager, auth_backend, refresh_backend from uuid import UUID user_db = MongoUserDatabase(user_collection) @@ -30,7 +30,7 @@ async def get_user_manager(db=Depends(get_user_db)): ) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/jwt/login") -get_current_user = fastapi_users.current_user(active=False, verified=False) +get_current_user = fastapi_users.current_user(active=False) app = FastAPI() diff --git a/backend/app/models.py b/backend/auth/models.py similarity index 89% rename from backend/app/models.py rename to backend/auth/models.py index d97a7038..dc6a6e83 100644 --- a/backend/app/models.py +++ b/backend/auth/models.py @@ -9,4 +9,3 @@ class UserDB(BaseModel): hashed_password: str is_active: bool = True is_superuser: bool = False - is_verified: bool = False \ No newline at end of file diff --git a/backend/app/schema.py b/backend/auth/schema.py similarity index 100% rename from backend/app/schema.py rename to backend/auth/schema.py diff --git a/backend/app/__init__.py b/backend/auth/users/__init__.py similarity index 100% rename from backend/app/__init__.py rename to backend/auth/users/__init__.py diff --git a/backend/app/users/manager.py b/backend/auth/users/manager.py similarity index 95% rename from backend/app/users/manager.py rename to backend/auth/users/manager.py index ee077908..5fefd655 100644 --- a/backend/app/users/manager.py +++ b/backend/auth/users/manager.py @@ -4,11 +4,11 @@ from fastapi.security import OAuth2PasswordRequestForm from fastapi_users.manager import BaseUserManager, UUIDIDMixin -from backend.app.models import UserDB +from backend.auth.models import UserDB from fastapi_users.authentication import JWTStrategy, AuthenticationBackend, CookieTransport, BearerTransport from fastapi_users.password import PasswordHelper -from backend.app.config import SECRET -from backend.app.schema import UserCreate +from backend.auth.config import SECRET +from backend.auth.schema import UserCreate import logging @@ -58,7 +58,6 @@ async def create(self, user_create: UserCreate, safe: bool = False, request=None user_dict["is_active"] = True user_dict["is_superuser"] = False - user_dict["is_verified"] = False user_dict.pop("password", None) created_user = await self.user_db.create(user_dict) diff --git a/backend/app/users/mongo_users.py b/backend/auth/users/mongo_users.py similarity index 80% rename from backend/app/users/mongo_users.py rename to backend/auth/users/mongo_users.py index cdbe4a89..0c504d27 100644 --- a/backend/app/users/mongo_users.py +++ b/backend/auth/users/mongo_users.py @@ -1,5 +1,5 @@ from fastapi_users.db.base import BaseUserDatabase -from backend.app.models import UserDB +from backend.auth.models import UserDB from motor.motor_asyncio import AsyncIOMotorCollection from uuid import UUID, uuid4 from typing import Optional, Dict, Any @@ -25,9 +25,6 @@ async def create(self, user: dict) -> dict: if "id" not in user: print("[mongo_users] User does not have an ID, generating one...") user["id"] = str(uuid4()) - user.setdefault("is_active", True) - user.setdefault("is_superuser", False) - user.setdefault("is_verified", False) await self.collection.insert_one(user) return user @@ -39,17 +36,13 @@ async def update( if update_dict: user_dict = user.model_dump() user_dict.update(update_dict) - await self.collection.replace_one( - {"id": str(user.id)}, - user_dict - ) - return UserDB(**user_dict) else: - await self.collection.replace_one( - {"id": str(user.id)}, - user.model_dump() - ) - return user + user_dict = user.model_dump() + + user_dict["id"] = str(user_dict["id"]) + + await self.collection.replace_one({"id": user_dict["id"]}, user_dict) + return UserDB(**user_dict) async def get_by_username(self, username: str) -> Optional[UserDB]: user = await self.collection.find_one({"username": username}) diff --git a/backend/app/users/__init__.py b/backend/profile/database/__init__.py similarity index 100% rename from backend/app/users/__init__.py rename to backend/profile/database/__init__.py diff --git a/backend/profile/db.py b/backend/profile/database/db.py similarity index 100% rename from backend/profile/db.py rename to backend/profile/database/db.py diff --git a/backend/profile/database/mongo_users.py b/backend/profile/database/mongo_users.py new file mode 100644 index 00000000..22f3f0f7 --- /dev/null +++ b/backend/profile/database/mongo_users.py @@ -0,0 +1,60 @@ +from fastapi_users.db.base import BaseUserDatabase +from backend.profile.models import UserDB +from motor.motor_asyncio import AsyncIOMotorCollection +from uuid import UUID, uuid4 +from typing import Optional, Dict, Any + + +class MongoUserDatabase(BaseUserDatabase[UserDB, UUID]): + def __init__(self, collection: AsyncIOMotorCollection): + self.collection = collection + + async def get(self, id: UUID) -> Optional[UserDB]: + user = await self.collection.find_one({"id": str(id)}) + return UserDB(**user) if user else None + + async def get_by_email(self, email: str) -> Optional[UserDB]: + user = await self.collection.find_one({"email": email}) + if user: + # Ensure the ID is properly converted + user["id"] = str(user["id"]) if isinstance(user.get("id"), UUID) else user.get("id") + return UserDB(**user) + return None + + async def create(self, user: dict) -> dict: + if "id" not in user: + print("[mongo_users] User does not have an ID, generating one...") + user["id"] = str(uuid4()) + await self.collection.insert_one(user) + return user + + async def update( + self, + user: UserDB, + update_dict: Dict[str, Any] = None, + ) -> UserDB: + if update_dict: + user_dict = user.model_dump() + user_dict.update(update_dict) + else: + user_dict = user.model_dump() + + user_dict["id"] = str(user_dict["id"]) + + await self.collection.replace_one({"id": user_dict["id"]}, user_dict) + return UserDB(**user_dict) + + async def get_by_username(self, username: str) -> Optional[UserDB]: + user = await self.collection.find_one({"username": username}) + if user: + user["id"] = str(user["id"]) if isinstance(user.get("id"), UUID) else user.get("id") + return UserDB(**user) + return None + + async def delete(self, user: UserDB) -> None: + await self.collection.delete_one({"id": str(user.id)}) + + async def _convert_to_userdb(self, user_dict: dict) -> UserDB: + # Convert MongoDB document to UserDB + user_dict["id"] = UUID(user_dict["id"]) if isinstance(user_dict.get("id"), str) else user_dict.get("id") + return UserDB(**user_dict) \ No newline at end of file diff --git a/backend/profile/models.py b/backend/profile/models.py index ec0c2eab..be7df37c 100644 --- a/backend/profile/models.py +++ b/backend/profile/models.py @@ -9,5 +9,4 @@ class UserDB(BaseModel): email: EmailStr hashed_password: str is_active: bool = True - is_superuser: bool = False - is_verified: bool = False \ No newline at end of file + is_superuser: bool = False \ No newline at end of file diff --git a/backend/profile/routers/profile.py b/backend/profile/routers/profile.py index 4949c39d..500c7eb6 100644 --- a/backend/profile/routers/profile.py +++ b/backend/profile/routers/profile.py @@ -1,32 +1,54 @@ -from fastapi import APIRouter, HTTPException -from backend.profile.db import db +from uuid import UUID +from fastapi import APIRouter, HTTPException, Depends +from backend.auth.db import db, user_db from backend.profile.schemas import UserProfile, UpdateName, UpdateEmail, UpdatePassword +from backend.profile.utils import get_current_user_id router = APIRouter(prefix="/api/profile", tags=["profile"]) @router.get("/{id}") async def get_profile(id: str): - user = await db.users.find_one({"_id": id}) + try: + user = await user_db.get(UUID(id)) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid UUID format") if not user: raise HTTPException(status_code=404, detail="User not found") return user @router.patch("/{id}/name") -async def update_name(id: str, body: UpdateName): - await db.users.update_one({"_id": id}, {"$set": {"name": body.name}}) +async def update_name(id: str, body: UpdateName): # user_id: str = Depends(get_current_user_id)): + # if id != user_id: + # raise HTTPException(status_code=403, detail="You can only modify your own profile") + user = await user_db.get(UUID(id)) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + user.name = body.name + await user_db.update(user) return {"status": "name updated"} @router.patch("/{id}/email") async def update_email(id: str, body: UpdateEmail): - await db.users.update_one({"_id": id}, {"$set": {"email": body.email}}) + user = await user_db.get(UUID(id)) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + user.email = body.email + await user_db.update(user) return {"status": "email updated"} @router.patch("/{id}/password") async def update_password(id: str, body: UpdatePassword): - # если используешь fastapi-users, здесь нужно через UserManager менять пароль - return {"status": "password updated"} + # fastapi-users UserManager + # return {"status": "password updated"} + raise HTTPException(status_code=400, detail="Password not supported") @router.delete("/{id}") async def delete_profile(id: str): - await db.users.delete_one({"_id": id}) + user = await user_db.get(UUID(id)) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + await user_db.delete(user) return {"status": "deleted"} \ No newline at end of file diff --git a/backend/profile/utils.py b/backend/profile/utils.py new file mode 100644 index 00000000..86ff0d0c --- /dev/null +++ b/backend/profile/utils.py @@ -0,0 +1,16 @@ +from fastapi import Depends, HTTPException, Header +import httpx + +async def get_current_user_id(authorization: str = Header(...)) -> str: + if not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Missing or invalid Authorization header") + + token = authorization.removeprefix("Bearer ").strip() + + async with httpx.AsyncClient() as client: + response = await client.get("http://auth-backend:8000/auth/verify", headers={"Authorization": f"Bearer {token}"}) + + if response.status_code != 200: + raise HTTPException(status_code=401, detail="Invalid or expired token") + + return response.json().get("user_id") \ No newline at end of file diff --git a/backend/tests/__init__.py b/backend/tests/auth_test/__init__.py similarity index 100% rename from backend/tests/__init__.py rename to backend/tests/auth_test/__init__.py diff --git a/backend/tests/conftest.py b/backend/tests/auth_test/conftest.py similarity index 91% rename from backend/tests/conftest.py rename to backend/tests/auth_test/conftest.py index 0d839e3d..673e560c 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/auth_test/conftest.py @@ -2,8 +2,8 @@ import pytest import pytest_asyncio -from backend.app.db import user_collection -from backend.app.main import app +from backend.auth.db import user_collection +from backend.auth.main import app @pytest_asyncio.fixture diff --git a/backend/tests/test_delete_user.py b/backend/tests/auth_test/test_delete_user.py similarity index 100% rename from backend/tests/test_delete_user.py rename to backend/tests/auth_test/test_delete_user.py diff --git a/backend/tests/test_get_curr_user.py b/backend/tests/auth_test/test_get_curr_user.py similarity index 100% rename from backend/tests/test_get_curr_user.py rename to backend/tests/auth_test/test_get_curr_user.py diff --git a/backend/tests/test_login.py b/backend/tests/auth_test/test_login.py similarity index 96% rename from backend/tests/test_login.py rename to backend/tests/auth_test/test_login.py index 0925b400..3206b428 100644 --- a/backend/tests/test_login.py +++ b/backend/tests/auth_test/test_login.py @@ -1,7 +1,7 @@ import pytest from fastapi import status -from backend.app.main import app +from backend.auth.main import app @pytest.mark.asyncio diff --git a/backend/tests/test_login_wrong_password.py b/backend/tests/auth_test/test_login_wrong_password.py similarity index 100% rename from backend/tests/test_login_wrong_password.py rename to backend/tests/auth_test/test_login_wrong_password.py diff --git a/backend/tests/test_password_validation.py b/backend/tests/auth_test/test_password_validation.py similarity index 100% rename from backend/tests/test_password_validation.py rename to backend/tests/auth_test/test_password_validation.py diff --git a/backend/tests/test_protection.py b/backend/tests/auth_test/test_protection.py similarity index 100% rename from backend/tests/test_protection.py rename to backend/tests/auth_test/test_protection.py diff --git a/backend/tests/test_register.py b/backend/tests/auth_test/test_register.py similarity index 100% rename from backend/tests/test_register.py rename to backend/tests/auth_test/test_register.py diff --git a/backend/tests/test_register_invalid_email.py b/backend/tests/auth_test/test_register_invalid_email.py similarity index 100% rename from backend/tests/test_register_invalid_email.py rename to backend/tests/auth_test/test_register_invalid_email.py diff --git a/backend/tests/test_registered_duplicate_email.py b/backend/tests/auth_test/test_registered_duplicate_email.py similarity index 100% rename from backend/tests/test_registered_duplicate_email.py rename to backend/tests/auth_test/test_registered_duplicate_email.py diff --git a/backend/tests/test_registered_duplicate_username.py b/backend/tests/auth_test/test_registered_duplicate_username.py similarity index 100% rename from backend/tests/test_registered_duplicate_username.py rename to backend/tests/auth_test/test_registered_duplicate_username.py diff --git a/backend/tests/test_update_user.py b/backend/tests/auth_test/test_update_user.py similarity index 100% rename from backend/tests/test_update_user.py rename to backend/tests/auth_test/test_update_user.py diff --git a/backend/tests/test_verify_token.py b/backend/tests/auth_test/test_verify_token.py similarity index 100% rename from backend/tests/test_verify_token.py rename to backend/tests/auth_test/test_verify_token.py diff --git a/backend/tests/profile_tests/conftest.py b/backend/tests/profile_tests/conftest.py index 229c8176..4d2fd99a 100644 --- a/backend/tests/profile_tests/conftest.py +++ b/backend/tests/profile_tests/conftest.py @@ -1,31 +1,37 @@ import httpx import pytest_asyncio -from backend.profile.db import user_collection -from backend.profile.main import app +from backend.auth.main import app as auth_app +from backend.profile.main import app as profile_app +from backend.auth.db import user_collection @pytest_asyncio.fixture -async def test_client(): - transport = httpx.ASGITransport(app=app) +async def auth_client(): + transport = httpx.ASGITransport(app=auth_app) + return httpx.AsyncClient(transport=transport, base_url="http://test") + +@pytest_asyncio.fixture +async def profile_client(): + transport = httpx.ASGITransport(app=profile_app) return httpx.AsyncClient(transport=transport, base_url="http://test") @pytest_asyncio.fixture(autouse=True) -async def cleanup(test_client): +async def cleanup(auth_client): await user_collection.delete_many({}) yield await user_collection.delete_many({}) - await test_client.aclose() + await auth_client.aclose() @pytest_asyncio.fixture -async def registered_user(test_client): +async def registered_user(auth_client): user_data = { "name": "Test User", "username": "testuser", "email": "test@example.com", "password": "TestPassword123" } - response = await test_client.post("/auth/register", json=user_data) + response = await auth_client.post("/auth/register", json=user_data) assert response.status_code == 201 return user_data, response.json() \ No newline at end of file diff --git a/backend/tests/profile_tests/test_get_info.py b/backend/tests/profile_tests/test_get_info.py index 53109d68..db2d7e2e 100644 --- a/backend/tests/profile_tests/test_get_info.py +++ b/backend/tests/profile_tests/test_get_info.py @@ -1,18 +1,12 @@ import pytest from fastapi import status -from backend.profile.main import app @pytest.mark.asyncio -async def test_get_info(test_client, registered_user): +async def test_get_info(auth_client, registered_user, profile_client): user_data, user = registered_user - print("Registered routes:") - for route in app.routes: - if hasattr(route, "path"): - print(f"{route.path}") - - login_response = await test_client.post( + login_response = await auth_client.post( "/auth/login", data={ "username": user_data["email"], @@ -22,9 +16,18 @@ async def test_get_info(test_client, registered_user): token = login_response.json().get("access_token") assert login_response.status_code == status.HTTP_200_OK - response = await test_client.get( - "/api/profile", + verify_response = await auth_client.get( + "/auth/verify", headers={"Authorization": f"Bearer {token}"} ) - - print(response.json()) \ No newline at end of file + assert verify_response.status_code == status.HTTP_200_OK + id = verify_response.json()["user_id"] + assert verify_response.json()["email"] == user_data["email"] + + response = await profile_client.get( + f"/api/profile/{id}", + headers={"Authorization": f"Bearer {token}"} + ) + + print(response.json()) + assert response.status_code == status.HTTP_200_OK diff --git a/backend/tests/profile_tests/test_update_email.py b/backend/tests/profile_tests/test_update_email.py new file mode 100644 index 00000000..cd3d88c7 --- /dev/null +++ b/backend/tests/profile_tests/test_update_email.py @@ -0,0 +1,42 @@ +import pytest +from fastapi import status + + +@pytest.mark.asyncio +async def test_get_info(auth_client, registered_user, profile_client): + user_data, user = registered_user + + login_response = await auth_client.post( + "/auth/login", + data={ + "username": user_data["email"], + "password": user_data["password"] + } + ) + token = login_response.json().get("access_token") + assert login_response.status_code == status.HTTP_200_OK + + # verify_response = await auth_client.get( + # "/auth/verify", + # headers={"Authorization": f"Bearer {token}"} + # ) + # assert verify_response.status_code == status.HTTP_200_OK + # id = verify_response.json()["user_id"] + + new_email = "newemail@example.com" + update_response = await profile_client.patch( + f"/api/profile/{str(user['id'])}/email", + json={"email": new_email}, + headers={"Authorization": f"Bearer {token}"} + ) + + assert update_response.status_code == status.HTTP_200_OK + assert update_response.json()["status"] == "email updated" + + profile_response = await profile_client.get( + f"/api/profile/{str(user['id'])}", + headers={"Authorization": f"Bearer {token}"} + ) + + assert profile_response.status_code == status.HTTP_200_OK + assert profile_response.json()["email"] == new_email From 3ffd9e1245f3a248cdaf312b28b85ea0469c232d Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Thu, 17 Jul 2025 15:38:33 +0300 Subject: [PATCH 063/152] onPaneContextMenu introduced --- .../codeComponents/PaneContextMenu.jsx | 91 +++++++++++++++++++ UI/src/components/pages/mainPage.jsx | 37 +++++--- UI/src/components/pages/mainPage/toolbar.jsx | 2 +- 3 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 UI/src/components/codeComponents/PaneContextMenu.jsx diff --git a/UI/src/components/codeComponents/PaneContextMenu.jsx b/UI/src/components/codeComponents/PaneContextMenu.jsx new file mode 100644 index 00000000..bfb03792 --- /dev/null +++ b/UI/src/components/codeComponents/PaneContextMenu.jsx @@ -0,0 +1,91 @@ +import React, { useCallback } from "react"; +import { useReactFlow } from "@xyflow/react"; + +export default function PaneContextMenu({ + copyElements, + pasteElements, + selectedElements, + onClose, + top, + left, + right, + bottom, + ...props + }) { + const { setNodes, setEdges } = useReactFlow(); + + const rotateSelectedNodes = useCallback( + (angle) => { + console.log("selectedElements", selectedElements); + if (!selectedElements?.nodes?.length) return; + const selectedNodeIds = new Set(selectedElements.nodes.map((n) => n.id)); + + setNodes((nodes) => + nodes.map((node) => { + if (selectedNodeIds.has(node.id)) { + const currentRotation = node.data?.rotation || 0; + const newRotation = (currentRotation + angle + 360) % 360; + return { + ...node, + data: { + ...node.data, + rotation: newRotation, + }, + }; + } + return node; + }) + ); + }, + [selectedElements, setNodes], + ); + + const deleteSelectedElements = useCallback(() => { + const selectedNodeIds = new Set(selectedElements.nodes.map(n => n.id)); + const selectedEdgeIds = new Set(selectedElements.edges.map(e => e.id)); + + setNodes(nodes => + nodes.filter(node => !selectedNodeIds.has(node.id)) + ); + + setEdges(edges => + edges.filter(edge => + !selectedEdgeIds.has(edge.id) && + !selectedNodeIds.has(edge.source) && + !selectedNodeIds.has(edge.target) + ) + ); + }, [selectedElements, setNodes, setEdges]); + + + return ( +
+ + + +
+ ); +} diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 96fd1a8d..de6a5298 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -28,6 +28,7 @@ import Settings from "./mainPage/settings.jsx"; import NodeContextMenu from "../codeComponents/NodeContextMenu.jsx"; import EdgeContextMenu from "../codeComponents/EdgeContextMenu.jsx"; +import PaneContextMenu from "../codeComponents/PaneContextMenu.jsx"; import { nodeTypes } from "../codeComponents/nodes.js"; import { IconMenu, IconSettings } from "../../../assets/ui-icons.jsx"; @@ -126,7 +127,7 @@ export default function Main() { const store = useStoreApi(); const { getInternalNode } = useReactFlow(); const [reactFlowInstance, setReactFlowInstance] = useState(null); - const [panOnDrag, setPanOnDrag] = useState([2]); + const [panOnDrag, setPanOnDrag] = useState(false); const socketRef = useRef(null); @@ -422,6 +423,8 @@ export default function Main() { [reactFlowInstance, setNodes, deselectAll, recordHistory], ); + const closeMenu = () => setMenu(null); + const onNodeContextMenu = useCallback((event, node) => { event.preventDefault(); const pane = ref.current.getBoundingClientRect(); @@ -445,7 +448,6 @@ export default function Main() { id: edge.id, name: edge.type, type: "edge", - position: menuPosition, top: menuPosition.top, left: menuPosition.left, right: menuPosition.right, @@ -453,11 +455,17 @@ export default function Main() { }); }, []); - const onPaneContextMenu = useCallback((event, edge) => { + const onPaneContextMenu = useCallback((event) => { event.preventDefault(); - const pane = ref.current.getBoundingClientRect(); - const menuPosition = calculateContextMenuPosition(event, pane); - setMenu(calculateContextMenuPosition(event, edge, pane, "pane")); + const paneRect = ref.current.getBoundingClientRect(); + const menuPosition = calculateContextMenuPosition(event, paneRect); + setMenu({ + type: "pane", + top: menuPosition.top, + left: menuPosition.left, + right: menuPosition.right, + bottom: menuPosition.bottom, + }); }, []); //Allows user to download circuit JSON @@ -572,9 +580,9 @@ export default function Main() { defaultEdgeOptions={{ type: activeWire, }} - onPaneContextMenu={onPaneContextMenu} onNodeContextMenu={onNodeContextMenu} onEdgeContextMenu={onEdgeContextMenu} + onPaneContextMenu={onPaneContextMenu} onConnect={onConnect} onNodeDragStop={onNodeDragStop} onDrop={onDrop} @@ -632,6 +640,16 @@ export default function Main() { )} + {menu && menu.type === "pane" && ( + + )} + { - setMenu(null); - }} + onClick={() => closeMenu()} /> { - setMenu(null); setOpenSettings(false); }} /> diff --git a/UI/src/components/pages/mainPage/toolbar.jsx b/UI/src/components/pages/mainPage/toolbar.jsx index 233bfb38..5a6194d8 100644 --- a/UI/src/components/pages/mainPage/toolbar.jsx +++ b/UI/src/components/pages/mainPage/toolbar.jsx @@ -76,7 +76,7 @@ export default function Toolbar({ className={`toolbarButton ${activeAction === "cursor" ? "active" : ""}`} onClick={() => { setActiveAction("cursor"); - setPanOnDrag([2]); + setPanOnDrag(false); }} disabled={activeAction === "cursor"} title={"Cursor (Ctrl+1)"} From 4d52e21954043f1df4721ab331f42b5448198d91 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Thu, 17 Jul 2025 18:28:08 +0300 Subject: [PATCH 064/152] disabled buttons color fixed --- UI/src/CSS/contextMenu.css | 17 +++++++++++++++-- UI/src/CSS/toolbar.css | 8 ++++++++ .../codeComponents/PaneContextMenu.jsx | 1 + 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/UI/src/CSS/contextMenu.css b/UI/src/CSS/contextMenu.css index 2cf52101..363540c2 100644 --- a/UI/src/CSS/contextMenu.css +++ b/UI/src/CSS/contextMenu.css @@ -20,8 +20,21 @@ background: var(--menu-lighter); } -.contextMenuButton:hover { - background: var(--main-4); +.contextMenuButton:disabled { + color: var(--secondary-text); +} + +.contextMenuButton:not(:disabled):hover { + transform: translateY(-3px); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); +} + +.contextMenuButton:not(:disabled):active { + transform: translateY(0px); + transition: 0.05s ease-out; + background-color: var(--select-2); + box-shadow: none; + border: var(--select-1) solid 0.05rem; } .selectWireType { diff --git a/UI/src/CSS/toolbar.css b/UI/src/CSS/toolbar.css index 9b834f54..5ca9ef7e 100644 --- a/UI/src/CSS/toolbar.css +++ b/UI/src/CSS/toolbar.css @@ -31,6 +31,14 @@ border-radius: 0.33rem; } +.toolbarButton:disabled .toolbarButtonIcon { + color: var(--secondary-text); +} + +.toolbarButton:disabled.active .toolbarButtonIcon { + color: var(--main-0); +} + .toolbarButton:not(:disabled):hover { transform: translateY(-3px); box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); diff --git a/UI/src/components/codeComponents/PaneContextMenu.jsx b/UI/src/components/codeComponents/PaneContextMenu.jsx index bfb03792..06719c33 100644 --- a/UI/src/components/codeComponents/PaneContextMenu.jsx +++ b/UI/src/components/codeComponents/PaneContextMenu.jsx @@ -69,6 +69,7 @@ export default function PaneContextMenu({ style={{ margin: "0.5rem" }} className={"contextMenuButton"} onClick={copyElements} + disabled={!selectedElements?.nodes?.length} > Copy From f3500065802818db7dee9b8664fc1800a1deb969 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Thu, 17 Jul 2025 19:36:25 +0300 Subject: [PATCH 065/152] paste button in context menu works properly --- UI/src/components/codeComponents/PaneContextMenu.jsx | 2 ++ UI/src/components/pages/mainPage.jsx | 1 + 2 files changed, 3 insertions(+) diff --git a/UI/src/components/codeComponents/PaneContextMenu.jsx b/UI/src/components/codeComponents/PaneContextMenu.jsx index 06719c33..4372798a 100644 --- a/UI/src/components/codeComponents/PaneContextMenu.jsx +++ b/UI/src/components/codeComponents/PaneContextMenu.jsx @@ -5,6 +5,7 @@ export default function PaneContextMenu({ copyElements, pasteElements, selectedElements, + clipboard, onClose, top, left, @@ -77,6 +78,7 @@ export default function PaneContextMenu({ style={{ margin: "0.5rem" }} className={"contextMenuButton"} onClick={pasteElements} + disabled={!clipboard?.nodes?.length} > Paste here diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index de6a5298..e0247d75 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -647,6 +647,7 @@ export default function Main() { copyElements={copyElements} pasteElements={pasteElements} onClose={closeMenu} + clipboard={clipboard} /> )} From fa366edb1cf7912142b9e9a22db5dddd2ec4a247 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Thu, 17 Jul 2025 19:40:52 +0300 Subject: [PATCH 066/152] cut button in context menu works properly --- .../codeComponents/PaneContextMenu.jsx | 59 +++++++++++-------- UI/src/components/pages/mainPage.jsx | 1 + 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/UI/src/components/codeComponents/PaneContextMenu.jsx b/UI/src/components/codeComponents/PaneContextMenu.jsx index 4372798a..de316eb2 100644 --- a/UI/src/components/codeComponents/PaneContextMenu.jsx +++ b/UI/src/components/codeComponents/PaneContextMenu.jsx @@ -4,6 +4,7 @@ import { useReactFlow } from "@xyflow/react"; export default function PaneContextMenu({ copyElements, pasteElements, + cutElements, selectedElements, clipboard, onClose, @@ -15,31 +16,31 @@ export default function PaneContextMenu({ }) { const { setNodes, setEdges } = useReactFlow(); - const rotateSelectedNodes = useCallback( - (angle) => { - console.log("selectedElements", selectedElements); - if (!selectedElements?.nodes?.length) return; - const selectedNodeIds = new Set(selectedElements.nodes.map((n) => n.id)); - - setNodes((nodes) => - nodes.map((node) => { - if (selectedNodeIds.has(node.id)) { - const currentRotation = node.data?.rotation || 0; - const newRotation = (currentRotation + angle + 360) % 360; - return { - ...node, - data: { - ...node.data, - rotation: newRotation, - }, - }; - } - return node; - }) - ); - }, - [selectedElements, setNodes], - ); + // const rotateSelectedNodes = useCallback( + // (angle) => { + // console.log("selectedElements", selectedElements); + // if (!selectedElements?.nodes?.length) return; + // const selectedNodeIds = new Set(selectedElements.nodes.map((n) => n.id)); + // + // setNodes((nodes) => + // nodes.map((node) => { + // if (selectedNodeIds.has(node.id)) { + // const currentRotation = node.data?.rotation || 0; + // const newRotation = (currentRotation + angle + 360) % 360; + // return { + // ...node, + // data: { + // ...node.data, + // rotation: newRotation, + // }, + // }; + // } + // return node; + // }) + // ); + // }, + // [selectedElements, setNodes], + // ); const deleteSelectedElements = useCallback(() => { const selectedNodeIds = new Set(selectedElements.nodes.map(n => n.id)); @@ -82,6 +83,14 @@ export default function PaneContextMenu({ > Paste here + +
+ )} + {menu && menu.type === "node" && ( )} From 3251681172221dc80784d522b11415ef125d0b7b Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Thu, 17 Jul 2025 21:27:41 +0300 Subject: [PATCH 069/152] css variables names changed according to naming conventions --- UI/src/CSS/circuits-menu.css | 30 ++++----- UI/src/CSS/context-menu.css | 10 +-- UI/src/CSS/dnd.css | 10 +-- UI/src/CSS/profile.css | 18 ++--- UI/src/CSS/select.css | 30 ++++----- UI/src/CSS/settings.css | 14 ++-- UI/src/CSS/switch.css | 6 +- UI/src/CSS/toolbar.css | 14 ++-- .../codeComponents/EdgeContextMenu.jsx | 4 +- .../codeComponents/NodeContextMenu.jsx | 8 +-- .../codeComponents/PaneContextMenu.jsx | 8 +-- UI/src/components/pages/mainPage.jsx | 8 +-- .../pages/mainPage/circuitsMenu.jsx | 8 +-- UI/src/components/pages/mainPage/select.jsx | 66 +++++++++---------- UI/src/components/pages/mainPage/settings.jsx | 20 +++--- UI/src/components/pages/mainPage/switch.jsx | 6 +- UI/src/components/pages/mainPage/toolbar.jsx | 44 ++++++------- UI/src/components/pages/profile.jsx | 16 ++--- 18 files changed, 160 insertions(+), 160 deletions(-) diff --git a/UI/src/CSS/circuits-menu.css b/UI/src/CSS/circuits-menu.css index 0314de56..694dda35 100644 --- a/UI/src/CSS/circuits-menu.css +++ b/UI/src/CSS/circuits-menu.css @@ -1,5 +1,5 @@ /*_______circuits menu_______*/ -.circuitsMenu { +.circuits-menu { /*width: 15.33rem;*/ overflow: auto; max-height: 70vh; @@ -21,29 +21,29 @@ } /* Global scrollbar styles */ -.circuitsMenu::-webkit-scrollbar { +.circuits-menu::-webkit-scrollbar { width: 8px; /* width of the vertical scrollbar */ } -.circuitsMenu::-webkit-scrollbar-track { +.circuits-menu::-webkit-scrollbar-track { background: transparent; } -.circuitsMenu::-webkit-scrollbar-thumb { +.circuits-menu::-webkit-scrollbar-thumb { background-color: transparent; /* Invisible by default */ border-radius: 4px; transition: background-color 0.2s ease; } -.circuitsMenu:hover::-webkit-scrollbar-thumb { +.circuits-menu:hover::-webkit-scrollbar-thumb { background: var(--main-4); /* hover effect */ } -.circuitsMenu:hover::-webkit-scrollbar-thumb:hover { +.circuits-menu:hover::-webkit-scrollbar-thumb:hover { background: var(--main-5); /* hover effect */ } -.circuitsMenu.open { +.circuits-menu.open { transition: 0.2s ease, background-color var(--ttime), @@ -52,19 +52,19 @@ left: 0; } -.circuitsMenuTitle { +.circuits-menu-title { margin-top: 0.66rem; text-align: center; font-size: 1.32rem; } -.buttonPicture { +.button-picture { width: 2.66rem; align-items: center; vertical-align: middle; } -.buttonText { +.button-text { vertical-align: bottom; font-size: 10px; text-align: center; @@ -79,7 +79,7 @@ align-items: center; } -.circuitsName { +.circuits-name { text-align: center; } @@ -118,7 +118,7 @@ } /*_______Open Menu_______*/ -.openCircuitsMenuButton { +.open-circuits-menu-button { position: fixed; top: calc(0.5rem + 5vh); left: 0.5rem; @@ -134,7 +134,7 @@ border var(--ttime); } -.openCircuitsMenuButtonIcon { +.open-circuits-menu-button-icon { color: var(--main-0); width: 1.4rem; height: 1.4rem; @@ -145,12 +145,12 @@ align-items: center; } -.openCircuitsMenuButton:hover { +.open-circuits-menu-button:hover { transition: 0.15s ease-out; background-color: var(--main-3); } -.openCircuitsMenuButton:active { +.open-circuits-menu-button:active { transition: 0.05s ease-out; background-color: var(--main-3); box-shadow: none; diff --git a/UI/src/CSS/context-menu.css b/UI/src/CSS/context-menu.css index 363540c2..06613bbb 100644 --- a/UI/src/CSS/context-menu.css +++ b/UI/src/CSS/context-menu.css @@ -8,7 +8,7 @@ color: var(--main-0); } -.contextMenuButton { +.context-menu-button { border: 1px solid var(--main-4); display: block; padding: 0.5rem; @@ -20,16 +20,16 @@ background: var(--menu-lighter); } -.contextMenuButton:disabled { +.context-menu-button:disabled { color: var(--secondary-text); } -.contextMenuButton:not(:disabled):hover { +.context-menu-button:not(:disabled):hover { transform: translateY(-3px); box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); } -.contextMenuButton:not(:disabled):active { +.context-menu-button:not(:disabled):active { transform: translateY(0px); transition: 0.05s ease-out; background-color: var(--select-2); @@ -37,7 +37,7 @@ border: var(--select-1) solid 0.05rem; } -.selectWireType { +.select-wire-type { padding: 0.5rem; margin: 0.5rem; } diff --git a/UI/src/CSS/dnd.css b/UI/src/CSS/dnd.css index 52593b96..a0ec06d0 100644 --- a/UI/src/CSS/dnd.css +++ b/UI/src/CSS/dnd.css @@ -1,5 +1,5 @@ /*______DragAndDrop______*/ -.dndnode { +.menu-element { padding: 8px; border: var(--main-4) 0.05rem solid; border-radius: 0.5rem; @@ -11,18 +11,18 @@ user-select: none; } -.dndnode:hover { +.menu-element:hover { transform: translateY(-3px); box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); } -.dndnode:active { +.menu-element:active { background: var(--main-4); transform: translateY(0px); box-shadow: none; } -.dndnode svg { +.menu-element svg { width: 50px; height: auto; user-select: none; @@ -60,7 +60,7 @@ justify-content: center; } -.switchNodeButton { +.switch-node-button { width: 70%; height: 70%; } diff --git a/UI/src/CSS/profile.css b/UI/src/CSS/profile.css index d64efc2a..37cd8cc8 100644 --- a/UI/src/CSS/profile.css +++ b/UI/src/CSS/profile.css @@ -24,12 +24,12 @@ margin-left: auto; } -.profileUserName { +.profile-user-name { color: var(--main-0); font-size: 1rem; } -.profileUserIcon { +.profile-user-icon { width: 3.5rem; height: 3.5rem; border-radius: 50%; @@ -52,14 +52,14 @@ padding: 2rem; } -.profileToEdit { +.profile-to-edit { display: flex; flex-direction: column; align-items: center; margin-bottom: 2rem; } -.userIconToEdit { +.user-icon-to-edit { width: 12rem; height: 12rem; border: 1px solid; @@ -69,14 +69,14 @@ background-color: white; } -.userNameToEdit { +.user-name-to-edit { color: var(--main-0); font-size: 1.5rem; font-weight: 600; margin-bottom: 1.5rem; } -.editProfileButton { +.edit-profile-button { color: var(--main-0); width: 100%; max-width: 15rem; @@ -88,7 +88,7 @@ transition: background-color 0.2s; } -.editProfileButton:hover { +.edit-profile-button:hover { background-color: var(--main-3); } @@ -98,13 +98,13 @@ font-size: 1.2rem; } -.projectsPanel { +.projects-panel { flex: 1; padding: 2rem; border-left: 1px solid #c1c1c1; } -.projectPanelName { +.project-panel-name { color: var(--main-0); font-size: 1.8rem; font-weight: bold; diff --git a/UI/src/CSS/select.css b/UI/src/CSS/select.css index a380dbdf..fc88d7ec 100644 --- a/UI/src/CSS/select.css +++ b/UI/src/CSS/select.css @@ -3,7 +3,7 @@ button { all: unset; } -.SelectTrigger { +.select-trigger { display: inline-flex; align-items: center; border-radius: 4px; @@ -22,22 +22,22 @@ button { border var(--ttime); } -.SelectTrigger:hover { +.select-trigger:hover { background-color: var(--main-4); transition: 0.08s ease; } -.SelectTrigger[data-placeholder] { +.select-trigger[data-placeholder] { color: #000000; transition: 0.08s ease; } -.SelectIcon { +.select-icon { color: var(--main-0); transition: 0.08s ease; } -.SelectContent { +.select-content { overflow: hidden; background-color: var(--menu-lighter); border: 0.05rem solid var(--main-4); @@ -48,12 +48,12 @@ button { z-index: 10000; } -.SelectViewport { +.select-viewport { padding: 5px; transition: 0.08s ease; } -.SelectItem { +.select-item { color: var(--main-0); font-size: 13px; line-height: 1; @@ -66,13 +66,13 @@ button { user-select: none; transition: 0.08s ease; } -.SelectItem[data-highlighted] { +.select-item[data-highlighted] { outline: none; background-color: var(--main-4); transition: 0.08s ease; } -.SelectLabel { +.select-label { padding: 0 25px; font-size: 12px; line-height: 25px; @@ -80,7 +80,7 @@ button { transition: 0.08s ease; } -.SelectItemIndicator { +.select-item-indicator { position: absolute; left: 0; width: 25px; @@ -90,7 +90,7 @@ button { transition: 0.08s ease; } -.SelectScrollButton { +.select-scroll-button { display: flex; align-items: center; justify-content: center; @@ -101,7 +101,7 @@ button { transition: 0.8s ease; } -.SelectTriggerWire { +.select-trigger-wire { display: flex; align-items: center; justify-content: space-between; @@ -115,15 +115,15 @@ button { text-align: left; } -.SelectTriggerWire:hover { +.select-trigger-wire:hover { background-color: var(--main-4); } -.SelectTriggerWire[data-placeholder] { +.select-trigger-wire[data-placeholder] { color: #000000; } -.SelectValueWrapper { +.select-value-wrapper { flex-grow: 1; overflow: hidden; text-overflow: ellipsis; diff --git a/UI/src/CSS/settings.css b/UI/src/CSS/settings.css index e351761d..c2abf059 100644 --- a/UI/src/CSS/settings.css +++ b/UI/src/CSS/settings.css @@ -1,4 +1,4 @@ -.settingsMenu { +.settings-menu { display: flex; overflow: auto; position: fixed; @@ -42,7 +42,7 @@ } } -.openSettingsButton { +.open-settings-button { z-index: 501; position: fixed; top: calc(0.5rem + 5vh); @@ -77,7 +77,7 @@ } } -.openSettingsButtonIcon { +.open-settings-button-icon { color: var(--main-0); width: 1.3rem; height: 1.3rem; @@ -85,7 +85,7 @@ display: block; } -.settingBlock { +.setting-block { display: flex; justify-content: space-between; align-items: flex-start; @@ -207,7 +207,7 @@ border-bottom: 0.05rem solid var(--main-5); } -.openProfileButton { +.open-profile-button { display: flex; margin-top: 0.5rem; margin-left: 5%; @@ -224,7 +224,7 @@ } } -.settingUserIcon { +.setting-user-icon { color: var(--main-0); background-color: white; width: 3rem; @@ -235,7 +235,7 @@ transition: background-color 0.55s ease-in-out; } -.settingUserName { +.setting-user-name { color: var(--main-0); margin-left: 0.5rem; font-size: 1rem; diff --git a/UI/src/CSS/switch.css b/UI/src/CSS/switch.css index 72b0137e..7a0eb8dc 100644 --- a/UI/src/CSS/switch.css +++ b/UI/src/CSS/switch.css @@ -3,7 +3,7 @@ button { all: unset; } -.SwitchRoot { +.switch-root { font-size: 3rem; width: 42px; height: 25px; @@ -17,7 +17,7 @@ button { } } -.SwitchThumb { +.switch-thumb { display: block; width: 21px; height: 21px; @@ -31,7 +31,7 @@ button { } } -.Label { +.label { color: white; font-size: 3rem; line-height: 1; diff --git a/UI/src/CSS/toolbar.css b/UI/src/CSS/toolbar.css index 5ca9ef7e..88de2752 100644 --- a/UI/src/CSS/toolbar.css +++ b/UI/src/CSS/toolbar.css @@ -17,7 +17,7 @@ border var(--ttime); } -.toolbarButton { +.toolbar-button { width: 2rem; height: 2rem; gap: 0.4rem; @@ -31,20 +31,20 @@ border-radius: 0.33rem; } -.toolbarButton:disabled .toolbarButtonIcon { +.toolbar-button:disabled .toolbar-button-icon { color: var(--secondary-text); } -.toolbarButton:disabled.active .toolbarButtonIcon { +.toolbar-button:disabled.active .toolbar-button-icon { color: var(--main-0); } -.toolbarButton:not(:disabled):hover { +.toolbar-button:not(:disabled):hover { transform: translateY(-3px); box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); } -.toolbarButton:not(:disabled):active { +.toolbar-button:not(:disabled):active { transform: translateY(0px); transition: 0.05s ease-out; background-color: var(--select-2); @@ -52,7 +52,7 @@ border: var(--select-1) solid 0.05rem; } -.toolbarButtonIcon { +.toolbar-button-icon { color: var(--main-0); width: 1.2rem; height: 1.2rem; @@ -62,7 +62,7 @@ align-items: center; } -.toolbarButton.active { +.toolbar-button.active { transition: 0.05s ease-out; background-color: var(--select-1); box-shadow: none; diff --git a/UI/src/components/codeComponents/EdgeContextMenu.jsx b/UI/src/components/codeComponents/EdgeContextMenu.jsx index f2ff9457..c6636eb2 100644 --- a/UI/src/components/codeComponents/EdgeContextMenu.jsx +++ b/UI/src/components/codeComponents/EdgeContextMenu.jsx @@ -50,11 +50,11 @@ export default function EdgeContextMenu({
diff --git a/UI/src/components/pages/mainPage/circuitsMenu.jsx b/UI/src/components/pages/mainPage/circuitsMenu.jsx index 48a3fb29..3c3adbd3 100644 --- a/UI/src/components/pages/mainPage/circuitsMenu.jsx +++ b/UI/src/components/pages/mainPage/circuitsMenu.jsx @@ -57,10 +57,10 @@ export default function CircuitsMenu({ ]; return ( -
+
-

Menu

+

Menu

@@ -82,7 +82,7 @@ export default function CircuitsMenu({ {item.gates.map((node) => (
onDragStart(e, node.id)} title={node.label} @@ -92,7 +92,7 @@ export default function CircuitsMenu({ SVGClassName="dndnode-icon" draggable="false" /> -
{node.label}
+
{node.label}
))} diff --git a/UI/src/components/pages/mainPage/select.jsx b/UI/src/components/pages/mainPage/select.jsx index 989a0393..54ec24fe 100644 --- a/UI/src/components/pages/mainPage/select.jsx +++ b/UI/src/components/pages/mainPage/select.jsx @@ -11,18 +11,18 @@ import "../../../CSS/select.css"; export const SelectCanvasBG = ({ currentBG, setCurrentBG }) => ( - + - + - - + + - + Dots Lines @@ -36,18 +36,18 @@ export const SelectCanvasBG = ({ currentBG, setCurrentBG }) => ( export const SelectTheme = ({ theme, setTheme }) => ( - + - + - - + + - + Light☀️ Dark🌙 @@ -64,20 +64,20 @@ export const SelectTheme = ({ theme, setTheme }) => ( export const SelectWireType = ({ wireType, setWireType }) => ( - -
+ +
- +
- - + + - + Bezier Step @@ -93,12 +93,12 @@ const SelectItem = React.forwardRef( ({ children, className, ...props }, forwardedRef) => { return ( {children} - + @@ -108,18 +108,18 @@ const SelectItem = React.forwardRef( export const SelectLogLevel = ({ currentLogLevel, setCurrentLogLevel }) => ( - + - + - - + + - + Critical Info @@ -137,20 +137,20 @@ export const SelectNotificationsPosition = ({ }) => ( - + - - + + - + Top center Bottom center @@ -163,18 +163,18 @@ export const SelectNotificationsPosition = ({ export const SelectPastePosition = ({ pastePosition, setPastePosition }) => ( - + - + - - + + - + Near the cursor Center of the screen diff --git a/UI/src/components/pages/mainPage/settings.jsx b/UI/src/components/pages/mainPage/settings.jsx index b70e52da..43fdd2df 100644 --- a/UI/src/components/pages/mainPage/settings.jsx +++ b/UI/src/components/pages/mainPage/settings.jsx @@ -37,7 +37,7 @@ export default function Settings({ const [currentTab, setCurrentTab] = useState(0); return ( -
+
@@ -167,32 +167,32 @@ export default function Toolbar({
@@ -200,11 +200,11 @@ export default function Toolbar({
{
-
UserName
- +
UserName
+
-
- - UserName +
+ + UserName
-
-
- My projects +
+ My projects
From 46bfa3b8d15b1eaf59638fd20bac1fbf9f84efcd Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Thu, 17 Jul 2025 23:03:20 +0300 Subject: [PATCH 070/152] name saves correctly, history event created after name change --- UI/src/CSS/name-editor.css | 33 ++++++++++++----- UI/src/CSS/tabs.css | 2 +- UI/src/CSS/toolbar.css | 2 ++ UI/src/components/pages/mainPage.jsx | 54 +++++++++++----------------- 4 files changed, 48 insertions(+), 43 deletions(-) diff --git a/UI/src/CSS/name-editor.css b/UI/src/CSS/name-editor.css index 87cf4ec2..5a299e33 100644 --- a/UI/src/CSS/name-editor.css +++ b/UI/src/CSS/name-editor.css @@ -4,8 +4,10 @@ left: 80px; padding: 12px; background: var(--menu-lighter); - color: #fff; - border-radius: 4px; + color: var(--main-0); + border: 1px solid var(--main-5); + border-radius: 0.5rem; + user-select: none; z-index: 10; } @@ -16,17 +18,32 @@ .name-editor input { padding: 6px 8px; - border-radius: 4px; - border: 1px solid #ccc; + border-radius: 0.5rem; + background-color: var(--menu-lighter); + border: 1px solid var(--main-0); + color: var(--main-0); width: 200px; } .name-editor .close-button { margin-left: 8px; padding: 6px 12px; - border-radius: 4px; - background: #444; - color: #fff; - border: none; + color: var(--main-0); cursor: pointer; + background-color: var(--menu-lighter); + border: var(--main-4) solid 0.05rem; + border-radius: 0.33rem; +} + +.name-editor .close-button:hover { + transform: translateY(-3px); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); +} + +.name-editor .close-button:active { + transform: translateY(0px); + transition: 0.05s ease-out; + background-color: var(--select-2); + box-shadow: none; + border: var(--select-1) solid 0.05rem; } diff --git a/UI/src/CSS/tabs.css b/UI/src/CSS/tabs.css index 3b6a11f8..de465c60 100644 --- a/UI/src/CSS/tabs.css +++ b/UI/src/CSS/tabs.css @@ -94,7 +94,7 @@ .name-text-area.editing { outline: 1px solid var(--main-0); border-radius: 2px; - background-color: var(--main-2); + background-color: var(--menu-lighter); } .tab-title { diff --git a/UI/src/CSS/toolbar.css b/UI/src/CSS/toolbar.css index 88de2752..c23f00a0 100644 --- a/UI/src/CSS/toolbar.css +++ b/UI/src/CSS/toolbar.css @@ -42,6 +42,7 @@ .toolbar-button:not(:disabled):hover { transform: translateY(-3px); box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); + cursor: pointer; } .toolbar-button:not(:disabled):active { @@ -92,6 +93,7 @@ .simulate-button:not(:disabled):hover { transform: translateY(-3px); box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); + cursor: pointer; } .simulate-button.idle { diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index b0968d9f..06c59acd 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -135,44 +135,30 @@ export default function Main() { const ignoreChangesRef = useRef(false); - const [selectedNode, setSelectedNode] = useState(null); - - const onSelectionChange = useCallback( - ({ nodes: selectedNodes, edges: selectedEdges }) => { - if (selectedNodes.length === 1 && selectedEdges.length === 0) { - setSelectedNode(selectedNodes[0]); - console.log(selectedNodes[0].name); - } else { - setSelectedNode(null); + const editableNode = useMemo(() => { + const selectedNodes = nodes.filter(n => n.selected); + const selectedEdges = edges.filter(e => e.selected); + if (selectedNodes.length === 1 && selectedEdges.length === 0) { + const node = selectedNodes[0]; + if (["inputNodeSwitch", "inputNodeButton", "outputNodeLed"].includes(node.type)) { + return node; } - }, - [] - ); + } + return null; + }, [nodes]); const handleNameChange = (e) => { + if (!editableNode) return; + const newName = e.target.value; - setSelectedNode((prev) => ({ - ...prev, - name: newName, - })); - setNodes((nds) => - nds.map((n) => - n.id === selectedNode.id - ? { - ...n, - name: newName, - } - : n + setNodes(nds => + nds.map(n => + n.id === editableNode.id ? { ...n, name: newName } : n ) ); + setTimeout(recordHistory, 0); }; - const canChangeName = - selectedNode && - ["inputNodeSwitch", "inputNodeButton", "outputNodeLed"].includes( - selectedNode.type - ); - const handleOpenClick = () => { if (fileInputRef.current) { fileInputRef.current.click(); @@ -611,7 +597,6 @@ export default function Main() { edges={edges} onNodesChange={onNodesChangeFromHook} onEdgesChange={onEdgesChangeFromHook} - onSelectionChange={onSelectionChange} defaultEdgeOptions={{ type: activeWire, }} @@ -667,19 +652,20 @@ export default function Main() { )} - {canChangeName && ( + {editableNode && (
From cfed4e3a09527641d3f572fbf78344f703e8e904 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:03:50 +0000 Subject: [PATCH 071/152] Automated formatting --- .../codeComponents/PaneContextMenu.jsx | 46 +++++++++---------- UI/src/components/pages/mainPage.jsx | 35 ++++++-------- UI/src/components/pages/mainPage/tabs.jsx | 7 ++- UI/src/components/pages/mainPage/toolbar.jsx | 10 +++- .../utils/calculateContextMenuPosition.js | 5 +- 5 files changed, 50 insertions(+), 53 deletions(-) diff --git a/UI/src/components/codeComponents/PaneContextMenu.jsx b/UI/src/components/codeComponents/PaneContextMenu.jsx index 102077da..8acc51fd 100644 --- a/UI/src/components/codeComponents/PaneContextMenu.jsx +++ b/UI/src/components/codeComponents/PaneContextMenu.jsx @@ -2,18 +2,18 @@ import React, { useCallback } from "react"; import { useReactFlow } from "@xyflow/react"; export default function PaneContextMenu({ - copyElements, - pasteElements, - cutElements, - selectedElements, - clipboard, - onClose, - top, - left, - right, - bottom, - ...props - }) { + copyElements, + pasteElements, + cutElements, + selectedElements, + clipboard, + onClose, + top, + left, + right, + bottom, + ...props +}) { const { setNodes, setEdges } = useReactFlow(); // const rotateSelectedNodes = useCallback( @@ -43,23 +43,21 @@ export default function PaneContextMenu({ // ); const deleteSelectedElements = useCallback(() => { - const selectedNodeIds = new Set(selectedElements.nodes.map(n => n.id)); - const selectedEdgeIds = new Set(selectedElements.edges.map(e => e.id)); + const selectedNodeIds = new Set(selectedElements.nodes.map((n) => n.id)); + const selectedEdgeIds = new Set(selectedElements.edges.map((e) => e.id)); - setNodes(nodes => - nodes.filter(node => !selectedNodeIds.has(node.id)) - ); + setNodes((nodes) => nodes.filter((node) => !selectedNodeIds.has(node.id))); - setEdges(edges => - edges.filter(edge => - !selectedEdgeIds.has(edge.id) && - !selectedNodeIds.has(edge.source) && - !selectedNodeIds.has(edge.target) - ) + setEdges((edges) => + edges.filter( + (edge) => + !selectedEdgeIds.has(edge.id) && + !selectedNodeIds.has(edge.source) && + !selectedNodeIds.has(edge.target), + ), ); }, [selectedElements, setNodes, setEdges]); - return (
{ - const selectedNodes = nodes.filter(n => n.selected); - const selectedEdges = edges.filter(e => e.selected); + const selectedNodes = nodes.filter((n) => n.selected); + const selectedEdges = edges.filter((e) => e.selected); if (selectedNodes.length === 1 && selectedEdges.length === 0) { const node = selectedNodes[0]; - if (["inputNodeSwitch", "inputNodeButton", "outputNodeLed"].includes(node.type)) { + if ( + ["inputNodeSwitch", "inputNodeButton", "outputNodeLed"].includes( + node.type, + ) + ) { return node; } } @@ -151,10 +155,8 @@ export default function Main() { if (!editableNode) return; const newName = e.target.value; - setNodes(nds => - nds.map(n => - n.id === editableNode.id ? { ...n, name: newName } : n - ) + setNodes((nds) => + nds.map((n) => (n.id === editableNode.id ? { ...n, name: newName } : n)), ); setTimeout(recordHistory, 0); }; @@ -654,31 +656,22 @@ export default function Main() { {editableNode && (
- + -
)} - {menu && menu.type === "node" && ( - - )} + {menu && menu.type === "node" && } - {menu && menu.type === "edge" && ( - - )} + {menu && menu.type === "edge" && } {menu && menu.type === "pane" && ( { e.preventDefault(); - const menuPosition = calculateContextMenuPosition(e, ref.current.getBoundingClientRect()); + const menuPosition = calculateContextMenuPosition( + e, + ref.current.getBoundingClientRect(), + ); setContextMenu({ tabId: tabId, top: menuPosition.top, diff --git a/UI/src/components/pages/mainPage/toolbar.jsx b/UI/src/components/pages/mainPage/toolbar.jsx index 5fcbcb1a..3f0efd32 100644 --- a/UI/src/components/pages/mainPage/toolbar.jsx +++ b/UI/src/components/pages/mainPage/toolbar.jsx @@ -96,7 +96,10 @@ export default function Toolbar({ disabled={activeAction === "hand"} title={"Hand (Ctrl+2)"} > - +
diff --git a/UI/src/components/utils/calculateContextMenuPosition.js b/UI/src/components/utils/calculateContextMenuPosition.js index 10434788..d575d28c 100644 --- a/UI/src/components/utils/calculateContextMenuPosition.js +++ b/UI/src/components/utils/calculateContextMenuPosition.js @@ -1,7 +1,4 @@ -export function calculateContextMenuPosition( - event, - containerRect, -) { +export function calculateContextMenuPosition(event, containerRect) { return { top: event.clientY < containerRect.height - 200 && event.clientY, left: event.clientX < containerRect.width - 200 && event.clientX, From 289f8dad2f9546ca9c69da3920ec5bdad31e22d4 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Thu, 17 Jul 2025 23:09:16 +0300 Subject: [PATCH 072/152] calculateContextMenuPosition tests corrected --- .../calculateContextMenuPosition.unit.test.js | 80 +++++-------------- 1 file changed, 21 insertions(+), 59 deletions(-) diff --git a/UI/src/components/utils/__tests__/unit tests/calculateContextMenuPosition.unit.test.js b/UI/src/components/utils/__tests__/unit tests/calculateContextMenuPosition.unit.test.js index 7c76f21d..eb942ed6 100644 --- a/UI/src/components/utils/__tests__/unit tests/calculateContextMenuPosition.unit.test.js +++ b/UI/src/components/utils/__tests__/unit tests/calculateContextMenuPosition.unit.test.js @@ -1,67 +1,29 @@ -import { calculateContextMenuPosition } from "../../calculateContextMenuPosition.js"; +import { calculatePosition } from "../../calculatePosition.js"; -describe("calculateContextMenuPosition", () => { - const node = { id: "node-1", type: "AND" }; +jest.mock("../../../constants/nodeSizes", () => ({ + NODE_SIZES: { + AND: { width: 80, height: 60 }, + OR: { width: 100, height: 70 }, + default: { width: 50, height: 50 }, + }, +})); - const eventFactory = (x, y) => ({ - clientX: x, - clientY: y, +describe("calculatePosition", () => { + test("calculates position for AND node", () => { + const rawPos = { x: 200, y: 150 }; + const result = calculatePosition(rawPos, "AND"); + expect(result).toEqual({ x: 160, y: 120 }); // 200 - 80/2, 150 - 60/2 }); - const container = { - width: 800, - height: 600, - }; - - test("positions context menu top-left", () => { - const event = eventFactory(100, 100); - const result = calculateContextMenuPosition(event, node, container); - expect(result).toEqual( - expect.objectContaining({ - top: 100, - left: 100, - right: false, - bottom: false, - }), - ); - }); - - test("positions context menu top-right", () => { - const event = eventFactory(750, 100); - const result = calculateContextMenuPosition(event, node, container); - expect(result).toEqual( - expect.objectContaining({ - top: 100, - left: false, - right: 50, - bottom: false, - }), - ); - }); - - test("positions context menu bottom-left", () => { - const event = eventFactory(100, 580); - const result = calculateContextMenuPosition(event, node, container); - expect(result).toEqual( - expect.objectContaining({ - top: false, - left: 100, - right: false, - bottom: 20, - }), - ); + test("calculates position for OR node", () => { + const rawPos = { x: 300, y: 250 }; + const result = calculatePosition(rawPos, "OR"); + expect(result).toEqual({ x: 250, y: 215 }); // 300 - 100/2, 250 - 70/2 }); - test("positions context menu bottom-right", () => { - const event = eventFactory(780, 580); - const result = calculateContextMenuPosition(event, node, container); - expect(result).toEqual( - expect.objectContaining({ - top: false, - left: false, - right: 20, - bottom: 20, - }), - ); + test("calculates position for unknown node type using default size", () => { + const rawPos = { x: 100, y: 100 }; + const result = calculatePosition(rawPos, "UNKNOWN"); + expect(result).toEqual({ x: 75, y: 75 }); // 100 - 50/2, 100 - 50/2 }); }); From 56ae6c8af3c29f64df9a7ef02ef62e1bd5243c34 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Thu, 17 Jul 2025 23:49:47 +0300 Subject: [PATCH 073/152] tabs renaming corrected --- UI/src/components/pages/mainPage.jsx | 1 + UI/src/components/pages/mainPage/tabs.jsx | 12 +++++++++--- .../calculateContextMenuPosition.unit.test.js | 6 +++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index fa840f49..068b7e12 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -339,6 +339,7 @@ export default function Main() { const { nodes: newNodes, edges: newEdges } = deselectAllUtil(nodes, edges); setNodes(newNodes); setEdges(newEdges); + recordHistory(); }, [nodes, edges, setNodes, setEdges]); const deleteSelectedElements = useCallback(() => { diff --git a/UI/src/components/pages/mainPage/tabs.jsx b/UI/src/components/pages/mainPage/tabs.jsx index 0c92f6fd..9c799722 100644 --- a/UI/src/components/pages/mainPage/tabs.jsx +++ b/UI/src/components/pages/mainPage/tabs.jsx @@ -65,9 +65,15 @@ export default function TabsContainer({ }; const updateTabTitle = (id, newTitle) => { - const updated = tabs.map((t) => - t.id === id ? { ...t, title: newTitle } : t, - ); + const updated = tabs.map((t) => { + if (t.id === id) { + const unselectedNodes = t.nodes.map(node => ({ ...node, selected: false })); + const unselectedEdges = t.edges.map(edge => ({ ...edge, selected: false })); + return { ...t, title: newTitle, nodes: unselectedNodes, edges: unselectedEdges }; + } else { + return t; + } + }); onTabsChange(updated); }; diff --git a/UI/src/components/utils/__tests__/unit tests/calculateContextMenuPosition.unit.test.js b/UI/src/components/utils/__tests__/unit tests/calculateContextMenuPosition.unit.test.js index eb942ed6..1c23d6cc 100644 --- a/UI/src/components/utils/__tests__/unit tests/calculateContextMenuPosition.unit.test.js +++ b/UI/src/components/utils/__tests__/unit tests/calculateContextMenuPosition.unit.test.js @@ -12,18 +12,18 @@ describe("calculatePosition", () => { test("calculates position for AND node", () => { const rawPos = { x: 200, y: 150 }; const result = calculatePosition(rawPos, "AND"); - expect(result).toEqual({ x: 160, y: 120 }); // 200 - 80/2, 150 - 60/2 + expect(result).toEqual({ x: 160, y: 120 }); }); test("calculates position for OR node", () => { const rawPos = { x: 300, y: 250 }; const result = calculatePosition(rawPos, "OR"); - expect(result).toEqual({ x: 250, y: 215 }); // 300 - 100/2, 250 - 70/2 + expect(result).toEqual({ x: 250, y: 215 }); }); test("calculates position for unknown node type using default size", () => { const rawPos = { x: 100, y: 100 }; const result = calculatePosition(rawPos, "UNKNOWN"); - expect(result).toEqual({ x: 75, y: 75 }); // 100 - 50/2, 100 - 50/2 + expect(result).toEqual({ x: 75, y: 75 }); }); }); From 23401a30a167e402664b5bcb64e4853ae6678013 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:50:14 +0000 Subject: [PATCH 074/152] Automated formatting --- UI/src/components/pages/mainPage/tabs.jsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/UI/src/components/pages/mainPage/tabs.jsx b/UI/src/components/pages/mainPage/tabs.jsx index 9c799722..e289b289 100644 --- a/UI/src/components/pages/mainPage/tabs.jsx +++ b/UI/src/components/pages/mainPage/tabs.jsx @@ -67,9 +67,20 @@ export default function TabsContainer({ const updateTabTitle = (id, newTitle) => { const updated = tabs.map((t) => { if (t.id === id) { - const unselectedNodes = t.nodes.map(node => ({ ...node, selected: false })); - const unselectedEdges = t.edges.map(edge => ({ ...edge, selected: false })); - return { ...t, title: newTitle, nodes: unselectedNodes, edges: unselectedEdges }; + const unselectedNodes = t.nodes.map((node) => ({ + ...node, + selected: false, + })); + const unselectedEdges = t.edges.map((edge) => ({ + ...edge, + selected: false, + })); + return { + ...t, + title: newTitle, + nodes: unselectedNodes, + edges: unselectedEdges, + }; } else { return t; } From 0fd8d5b7969159fbe4a6e2239e78d9bc9a6e4f70 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Fri, 18 Jul 2025 00:11:48 +0300 Subject: [PATCH 075/152] tabs renaming only update the title now --- UI/src/components/pages/mainPage/tabs.jsx | 23 +++-------------------- UI/src/components/utils/hotkeyHandler.js | 6 ++++++ 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/UI/src/components/pages/mainPage/tabs.jsx b/UI/src/components/pages/mainPage/tabs.jsx index e289b289..fb33c52f 100644 --- a/UI/src/components/pages/mainPage/tabs.jsx +++ b/UI/src/components/pages/mainPage/tabs.jsx @@ -65,26 +65,9 @@ export default function TabsContainer({ }; const updateTabTitle = (id, newTitle) => { - const updated = tabs.map((t) => { - if (t.id === id) { - const unselectedNodes = t.nodes.map((node) => ({ - ...node, - selected: false, - })); - const unselectedEdges = t.edges.map((edge) => ({ - ...edge, - selected: false, - })); - return { - ...t, - title: newTitle, - nodes: unselectedNodes, - edges: unselectedEdges, - }; - } else { - return t; - } - }); + const updated = tabs.map((t) => + t.id === id ? { ...t, title: newTitle } : t + ); onTabsChange(updated); }; diff --git a/UI/src/components/utils/hotkeyHandler.js b/UI/src/components/utils/hotkeyHandler.js index 47285076..0fcb8fd4 100644 --- a/UI/src/components/utils/hotkeyHandler.js +++ b/UI/src/components/utils/hotkeyHandler.js @@ -1,4 +1,10 @@ export function hotkeyHandler(e, context) { + if (document.activeElement.tagName === 'INPUT' || + document.activeElement.tagName === 'TEXTAREA' || + document.activeElement.isContentEditable) { + return; + } + const { openSettings, setOpenSettings, From 595ce9c213958c1eea684a72804f22c1f1f69099 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 21:12:18 +0000 Subject: [PATCH 076/152] Automated formatting --- UI/src/components/pages/mainPage/tabs.jsx | 2 +- UI/src/components/utils/hotkeyHandler.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/UI/src/components/pages/mainPage/tabs.jsx b/UI/src/components/pages/mainPage/tabs.jsx index fb33c52f..0c92f6fd 100644 --- a/UI/src/components/pages/mainPage/tabs.jsx +++ b/UI/src/components/pages/mainPage/tabs.jsx @@ -66,7 +66,7 @@ export default function TabsContainer({ const updateTabTitle = (id, newTitle) => { const updated = tabs.map((t) => - t.id === id ? { ...t, title: newTitle } : t + t.id === id ? { ...t, title: newTitle } : t, ); onTabsChange(updated); }; diff --git a/UI/src/components/utils/hotkeyHandler.js b/UI/src/components/utils/hotkeyHandler.js index 0fcb8fd4..b264db0c 100644 --- a/UI/src/components/utils/hotkeyHandler.js +++ b/UI/src/components/utils/hotkeyHandler.js @@ -1,7 +1,9 @@ export function hotkeyHandler(e, context) { - if (document.activeElement.tagName === 'INPUT' || - document.activeElement.tagName === 'TEXTAREA' || - document.activeElement.isContentEditable) { + if ( + document.activeElement.tagName === "INPUT" || + document.activeElement.tagName === "TEXTAREA" || + document.activeElement.isContentEditable + ) { return; } From 4b9a3375442b207dd37a82a6ca2fb759d788f2ee Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Fri, 18 Jul 2025 00:23:53 +0300 Subject: [PATCH 077/152] hotkeyHandler code simplified, tests updated --- .../unit tests/hotkeyHandler.unit.test.js | 112 ++++++++++++++- UI/src/components/utils/hotkeyHandler.js | 135 ++++++++---------- 2 files changed, 169 insertions(+), 78 deletions(-) diff --git a/UI/src/components/utils/__tests__/unit tests/hotkeyHandler.unit.test.js b/UI/src/components/utils/__tests__/unit tests/hotkeyHandler.unit.test.js index cabd7b0a..a0ad6d2c 100644 --- a/UI/src/components/utils/__tests__/unit tests/hotkeyHandler.unit.test.js +++ b/UI/src/components/utils/__tests__/unit tests/hotkeyHandler.unit.test.js @@ -7,8 +7,8 @@ describe("hotkeyHandler", () => { copyElements: jest.fn(), cutElements: jest.fn(), pasteElements: jest.fn(), - handleSelectAll: jest.fn(), - handleDeselectAll: jest.fn(), + selectAll: jest.fn(), + deselectAll: jest.fn(), saveCircuit: jest.fn(), handleSimulateClick: jest.fn(), simulateState: "idle", @@ -20,6 +20,8 @@ describe("hotkeyHandler", () => { setActiveAction: jest.fn(), setPanOnDrag: jest.fn(), setActiveWire: jest.fn(), + undo: jest.fn(), + redo: jest.fn(), }); const makeEvent = (key, ctrlKey = true, shiftKey = false) => ({ @@ -45,6 +47,48 @@ describe("hotkeyHandler", () => { expect(ctx.cutElements).toHaveBeenCalled(); }); + it("calls pasteElements on Ctrl+V", () => { + const ctx = createContext(); + const e = makeEvent("v"); + hotkeyHandler(e, ctx); + expect(ctx.pasteElements).toHaveBeenCalled(); + }); + + it("calls selectAll on Ctrl+A", () => { + const ctx = createContext(); + const e = makeEvent("a"); + hotkeyHandler(e, ctx); + expect(ctx.selectAll).toHaveBeenCalled(); + }); + + it("calls deselectAll on Ctrl+D", () => { + const ctx = createContext(); + const e = makeEvent("d"); + hotkeyHandler(e, ctx); + expect(ctx.deselectAll).toHaveBeenCalled(); + }); + + it("calls undo on Ctrl+Z", () => { + const ctx = createContext(); + const e = makeEvent("z"); + hotkeyHandler(e, ctx); + expect(ctx.undo).toHaveBeenCalled(); + }); + + it("calls redo on Ctrl+Y", () => { + const ctx = createContext(); + const e = makeEvent("y"); + hotkeyHandler(e, ctx); + expect(ctx.redo).toHaveBeenCalled(); + }); + + it("calls redo on Ctrl+Shift+Z", () => { + const ctx = createContext(); + const e = makeEvent("z", true, true); + hotkeyHandler(e, ctx); + expect(ctx.redo).toHaveBeenCalled(); + }); + it("calls setOpenSettings on Ctrl+Shift+S", () => { const ctx = createContext(); const e = makeEvent("s", true, true); @@ -52,6 +96,13 @@ describe("hotkeyHandler", () => { expect(ctx.setOpenSettings).toHaveBeenCalled(); }); + it("calls saveCircuit on Ctrl+S", () => { + const ctx = createContext(); + const e = makeEvent("s"); + hotkeyHandler(e, ctx); + expect(ctx.saveCircuit).toHaveBeenCalled(); + }); + it("calls handleSimulateClick on Ctrl+Shift+R", () => { const ctx = createContext(); const e = makeEvent("r", true, true); @@ -65,6 +116,13 @@ describe("hotkeyHandler", () => { }); }); + it("calls handleOpenClick on Ctrl+O", () => { + const ctx = createContext(); + const e = makeEvent("o"); + hotkeyHandler(e, ctx); + expect(ctx.handleOpenClick).toHaveBeenCalled(); + }); + it('calls setActiveAction and setPanOnDrag on "1"', () => { const ctx = createContext(); const e = makeEvent("1", false); @@ -72,4 +130,54 @@ describe("hotkeyHandler", () => { expect(ctx.setActiveAction).toHaveBeenCalledWith("cursor"); expect(ctx.setPanOnDrag).toHaveBeenCalledWith([2]); }); + + it('calls setActiveAction and setPanOnDrag on "2"', () => { + const ctx = createContext(); + const e = makeEvent("2", false); + hotkeyHandler(e, ctx); + expect(ctx.setActiveAction).toHaveBeenCalledWith("hand"); + expect(ctx.setPanOnDrag).toHaveBeenCalledWith(true); + }); + + it('calls setActiveAction on "3"', () => { + const ctx = createContext(); + const e = makeEvent("3", false); + hotkeyHandler(e, ctx); + expect(ctx.setActiveAction).toHaveBeenCalledWith("eraser"); + }); + + it('calls setActiveAction on "4"', () => { + const ctx = createContext(); + const e = makeEvent("4", false); + hotkeyHandler(e, ctx); + expect(ctx.setActiveAction).toHaveBeenCalledWith("text"); + }); + + it('calls setActiveWire on "5"', () => { + const ctx = createContext(); + const e = makeEvent("5", false); + hotkeyHandler(e, ctx); + expect(ctx.setActiveWire).toHaveBeenCalledWith("default"); + }); + + it('calls setActiveWire on "6"', () => { + const ctx = createContext(); + const e = makeEvent("6", false); + hotkeyHandler(e, ctx); + expect(ctx.setActiveWire).toHaveBeenCalledWith("step"); + }); + + it('calls setActiveWire on "7"', () => { + const ctx = createContext(); + const e = makeEvent("7", false); + hotkeyHandler(e, ctx); + expect(ctx.setActiveWire).toHaveBeenCalledWith("straight"); + }); + + it("calls setOpenSettings(false) on Escape if settings are open", () => { + const ctx = createContext(); + const e = makeEvent("Escape", false); + hotkeyHandler(e, ctx); + expect(ctx.setOpenSettings).toHaveBeenCalledWith(false); + }); }); diff --git a/UI/src/components/utils/hotkeyHandler.js b/UI/src/components/utils/hotkeyHandler.js index b264db0c..ba6ce826 100644 --- a/UI/src/components/utils/hotkeyHandler.js +++ b/UI/src/components/utils/hotkeyHandler.js @@ -7,6 +7,8 @@ export function hotkeyHandler(e, context) { return; } + if (!context) return; + const { openSettings, setOpenSettings, @@ -31,124 +33,105 @@ export function hotkeyHandler(e, context) { } = context; const isCtrlOrCmd = e.ctrlKey || e.metaKey; + const key = e.key.toLowerCase(); + // Ctrl/Cmd + Key combinations if (isCtrlOrCmd) { - switch (e.key.toLowerCase()) { + switch (key) { case "c": case "с": e.preventDefault(); - copyElements(); + copyElements?.(); return; case "x": case "ч": e.preventDefault(); - cutElements(); + cutElements?.(); return; case "v": case "м": e.preventDefault(); - pasteElements(); + pasteElements?.(); return; case "a": case "ф": e.preventDefault(); - selectAll(); + selectAll?.(); return; case "d": case "в": e.preventDefault(); - deselectAll(); + deselectAll?.(); return; case "z": case "я": - e.preventDefault(); - undo(); + if (e.shiftKey) { + e.preventDefault(); + redo?.(); + } else { + e.preventDefault(); + undo?.(); + } return; case "y": case "н": e.preventDefault(); - redo(); + redo?.(); + return; + case "s": + case "ы": + e.preventDefault(); + if (e.shiftKey) { + setOpenSettings?.((prev) => !prev); + } else { + saveCircuit?.(); + } + return; + case "r": + case "к": + if (e.shiftKey) { + e.preventDefault(); + handleSimulateClick?.({ + simulateState, + setSimulateState, + socketRef, + nodes, + edges, + }); + } + return; + case "o": + case "щ": + e.preventDefault(); + handleOpenClick?.(); return; } } - if ( - isCtrlOrCmd && - e.shiftKey && - (e.key.toLowerCase() === "s" || e.key.toLowerCase() === "ы") - ) { - e.preventDefault(); - setOpenSettings((prev) => !prev); - return; - } - - if ( - isCtrlOrCmd && - (e.key.toLowerCase() === "s" || e.key.toLowerCase() === "ы") - ) { - e.preventDefault(); - saveCircuit(); - return; - } - - if ( - isCtrlOrCmd && - e.shiftKey && - (e.key.toLowerCase() === "r" || e.key.toLowerCase() === "к") - ) { - e.preventDefault(); - handleSimulateClick({ - simulateState, - setSimulateState, - socketRef, - nodes, - edges, - }); - return; - } - - if ( - isCtrlOrCmd && - (e.key.toLowerCase() === "o" || e.key.toLowerCase() === "щ") - ) { - e.preventDefault(); - handleOpenClick(); - return; - } - - if ( - isCtrlOrCmd && - e.shiftKey && - (e.key.toLowerCase() === "z" || e.key.toLowerCase() === "я") - ) { - e.preventDefault(); - setOpenSettings((prev) => !prev); - return; - } - + // Single-key hotkeys (not combined with Ctrl/Cmd) const hotkeys = { 1: () => { - setActiveAction("cursor"); - setPanOnDrag([2]); + setActiveAction?.("cursor"); + setPanOnDrag?.([2]); }, 2: () => { - setActiveAction("hand"); - setPanOnDrag(true); + setActiveAction?.("hand"); + setPanOnDrag?.(true); }, - 3: () => setActiveAction("eraser"), - 4: () => setActiveAction("text"), - 5: () => setActiveWire("default"), - 6: () => setActiveWire("step"), - 7: () => setActiveWire("straight"), + 3: () => setActiveAction?.("eraser"), + 4: () => setActiveAction?.("text"), + 5: () => setActiveWire?.("default"), + 6: () => setActiveWire?.("step"), + 7: () => setActiveWire?.("straight"), }; - if (hotkeys[e.key]) { + if (hotkeys[key]) { e.preventDefault(); - hotkeys[e.key](); + hotkeys[key](); return; } - if (e.key === "Escape" && openSettings) { - setOpenSettings(false); + if (key === "escape" && openSettings) { + setOpenSettings?.(false); } } From 1ce3ed97b0d4f3e7daa6557d60a37e2c72521f67 Mon Sep 17 00:00:00 2001 From: doshq Date: Fri, 18 Jul 2025 01:35:53 +0300 Subject: [PATCH 078/152] Integrated PostgreSQL, reformat get profile unit test --- backend/profile/config.py | 20 ++-- backend/profile/database/db.py | 44 +++++++- backend/profile/database/mongo_users.py | 60 ----------- backend/profile/database/postgres_users.py | 93 ++++++++++++++++ backend/profile/main.py | 10 +- backend/profile/models.py | 43 +++++--- backend/profile/routers/profile.py | 96 +++++++++-------- backend/profile/routers/project.py | 100 +++++++++--------- backend/profile/schemas.py | 32 +++++- backend/profile/utils.py | 15 ++- backend/profile/verify.py | 24 ----- backend/tests/profile_tests/conftest.py | 25 +++-- backend/tests/profile_tests/test_get_info.py | 18 ++-- .../tests/profile_tests/test_update_email.py | 2 +- docker-compose.yml | 4 + 15 files changed, 346 insertions(+), 240 deletions(-) delete mode 100644 backend/profile/database/mongo_users.py create mode 100644 backend/profile/database/postgres_users.py delete mode 100644 backend/profile/verify.py diff --git a/backend/profile/config.py b/backend/profile/config.py index 4bc3856a..7097d541 100644 --- a/backend/profile/config.py +++ b/backend/profile/config.py @@ -3,13 +3,13 @@ from dotenv import load_dotenv -dotenv_path = Path('.env') -load_dotenv() - -MONGO_URI = os.getenv("MONGO_URI") -SECRET = os.getenv("SECRET") - -if not MONGO_URI: - raise ValueError("MONGO_URI not set in environment variables") -if not SECRET: - raise ValueError("SECRET not set in environment variables") \ No newline at end of file +# dotenv_path = Path('.env') +# load_dotenv() +# +# MONGO_URI = os.getenv("MONGO_URI") +# SECRET = os.getenv("SECRET") +# +# if not MONGO_URI: +# raise ValueError("MONGO_URI not set in environment variables") +# if not SECRET: +# raise ValueError("SECRET not set in environment variables") \ No newline at end of file diff --git a/backend/profile/database/db.py b/backend/profile/database/db.py index 4e5c755e..932eb04c 100644 --- a/backend/profile/database/db.py +++ b/backend/profile/database/db.py @@ -1,6 +1,40 @@ -from backend.profile.config import MONGO_URI -from motor.motor_asyncio import AsyncIOMotorClient +import os +from typing import Any, AsyncGenerator +from fastapi import Depends +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession +from sqlalchemy.ext.asyncio.session import AsyncSession -client = AsyncIOMotorClient(MONGO_URI) -db = client["visual-circuit-designer"] -user_collection = db["Projects"] +from backend.profile.database.postgres_users import PostgreSQLUserDatabase +from backend.profile.models import User +from backend.profile.schemas import UserDB, UserProfile + +try: + DATABASE_URL = ( + f"postgresql+asyncpg://{os.environ['POSTGRES_USER']}:" + f"{os.environ['POSTGRES_PASSWORD']}@" + f"{os.environ['POSTGRES_HOST']}:" + f"{os.environ['POSTGRES_PORT']}/" + f"{os.environ['POSTGRES_DB']}" + ) +except KeyError: + DATABASE_URL = ( + f"postgresql+asyncpg://vcd:" + f"pgpwd4vcd@" + f"localhost:" + f"5432/" + f"vcd" + ) + +engine = create_async_engine(DATABASE_URL, echo=True) +async_session_maker = async_sessionmaker(engine, expire_on_commit=False) + +async def get_async_session() -> AsyncGenerator[AsyncSession, Any]: + async with async_session_maker() as session: + yield session + +async def get_user_db(session: AsyncSession = Depends(get_async_session)): + yield PostgreSQLUserDatabase( + session=session, + user_model=User, # ваша SQLAlchemy модель + user_db_model=UserDB # ваша Pydantic схема + ) \ No newline at end of file diff --git a/backend/profile/database/mongo_users.py b/backend/profile/database/mongo_users.py deleted file mode 100644 index 22f3f0f7..00000000 --- a/backend/profile/database/mongo_users.py +++ /dev/null @@ -1,60 +0,0 @@ -from fastapi_users.db.base import BaseUserDatabase -from backend.profile.models import UserDB -from motor.motor_asyncio import AsyncIOMotorCollection -from uuid import UUID, uuid4 -from typing import Optional, Dict, Any - - -class MongoUserDatabase(BaseUserDatabase[UserDB, UUID]): - def __init__(self, collection: AsyncIOMotorCollection): - self.collection = collection - - async def get(self, id: UUID) -> Optional[UserDB]: - user = await self.collection.find_one({"id": str(id)}) - return UserDB(**user) if user else None - - async def get_by_email(self, email: str) -> Optional[UserDB]: - user = await self.collection.find_one({"email": email}) - if user: - # Ensure the ID is properly converted - user["id"] = str(user["id"]) if isinstance(user.get("id"), UUID) else user.get("id") - return UserDB(**user) - return None - - async def create(self, user: dict) -> dict: - if "id" not in user: - print("[mongo_users] User does not have an ID, generating one...") - user["id"] = str(uuid4()) - await self.collection.insert_one(user) - return user - - async def update( - self, - user: UserDB, - update_dict: Dict[str, Any] = None, - ) -> UserDB: - if update_dict: - user_dict = user.model_dump() - user_dict.update(update_dict) - else: - user_dict = user.model_dump() - - user_dict["id"] = str(user_dict["id"]) - - await self.collection.replace_one({"id": user_dict["id"]}, user_dict) - return UserDB(**user_dict) - - async def get_by_username(self, username: str) -> Optional[UserDB]: - user = await self.collection.find_one({"username": username}) - if user: - user["id"] = str(user["id"]) if isinstance(user.get("id"), UUID) else user.get("id") - return UserDB(**user) - return None - - async def delete(self, user: UserDB) -> None: - await self.collection.delete_one({"id": str(user.id)}) - - async def _convert_to_userdb(self, user_dict: dict) -> UserDB: - # Convert MongoDB document to UserDB - user_dict["id"] = UUID(user_dict["id"]) if isinstance(user_dict.get("id"), str) else user_dict.get("id") - return UserDB(**user_dict) \ No newline at end of file diff --git a/backend/profile/database/postgres_users.py b/backend/profile/database/postgres_users.py new file mode 100644 index 00000000..ed061d32 --- /dev/null +++ b/backend/profile/database/postgres_users.py @@ -0,0 +1,93 @@ +from typing import Any, Dict, Optional, Type, TypeVar +from fastapi_users.db.base import BaseUserDatabase +from sqlalchemy import select, delete, update, insert +from sqlalchemy.ext.asyncio import AsyncSession +from backend.profile.models import User as UserModel # SQLAlchemy модель +from backend.profile.schemas import UserDB + +UP = TypeVar("UP", bound=UserDB) + + +class PostgreSQLUserDatabase(BaseUserDatabase[UP, int]): + def __init__( + self, + session: AsyncSession, + user_model: Type[UserModel], + user_db_model: Type[UP] + ): + self.session = session + self.user_model = user_model + self.user_db_model = user_db_model + + async def get(self, id: int) -> Optional[UP]: + stmt = select(self.user_model).where(self.user_model.id == id) + result = await self.session.execute(stmt) + user = result.scalar_one_or_none() + if user is None: + return None + return self._convert_to_userdb(user) + + async def get_by_email(self, email: str) -> Optional[UP]: + stmt = select(self.user_model).where(self.user_model.email == email) + result = await self.session.execute(stmt) + user = result.scalar_one_or_none() + if user is None: + return None + return self._convert_to_userdb(user) + + async def get_by_username(self, username: str) -> Optional[UP]: + stmt = select(self.user_model).where(self.user_model.username == username) + result = await self.session.execute(stmt) + user = result.scalar_one_or_none() + if user is None: + return None + return self._convert_to_userdb(user) + + async def create(self, user_dict: Dict[str, Any]) -> UP: + # Удаляем ID если он есть, так как он генерируется базой + user_dict.pop("id", None) + + stmt = insert(self.user_model).values(**user_dict).returning(self.user_model) + result = await self.session.execute(stmt) + await self.session.commit() + user = result.scalar_one() + return self._convert_to_userdb(user) + + async def update( + self, + user: UP, + update_dict: Dict[str, Any] + ) -> UP: + stmt = ( + update(self.user_model) + .where(self.user_model.id == user.id) + .values(**update_dict) + .returning(self.user_model) + ) + result = await self.session.execute(stmt) + await self.session.commit() + updated_user = result.scalar_one() + return self._convert_to_userdb(updated_user) + + async def delete(self, user: UP) -> None: + stmt = delete(self.user_model).where(self.user_model.id == user.id) + await self.session.execute(stmt) + await self.session.commit() + + def _convert_to_userdb(self, user: UserModel) -> UP: + """Конвертирует SQLAlchemy модель в Pydantic UserDB схему""" + user_dict = { + "id": user.id, + "username": user.username, + "name": user.name, + "email": user.email, + "hashed_password": user.password_hash, + "created_at": user.created_at, + "salt": user.salt, + # Добавляем обязательные поля со значениями по умолчанию + "is_active": True, + "is_superuser": False, + "is_verified": True + + } + return self.user_db_model(**user_dict) \ No newline at end of file diff --git a/backend/profile/main.py b/backend/profile/main.py index 522c3196..dcafbc25 100644 --- a/backend/profile/main.py +++ b/backend/profile/main.py @@ -1,16 +1,8 @@ import warnings from fastapi import FastAPI - from backend.profile.routers import profile, project -try: - from backend.profile.verify import temp_router -except (ModuleNotFoundError, ImportError): - # warnings.warn("Verify handler not found, skipping...") - temp_router = None app = FastAPI() app.include_router(profile.router) -app.include_router(project.router) -if temp_router: - app.include_router(temp_router, prefix="/auth") +# app.include_router(project.router) diff --git a/backend/profile/models.py b/backend/profile/models.py index be7df37c..a15e70d2 100644 --- a/backend/profile/models.py +++ b/backend/profile/models.py @@ -1,12 +1,31 @@ -from uuid import UUID -from pydantic import BaseModel, EmailStr - - -class UserDB(BaseModel): - id: UUID - name: str - username: str - email: EmailStr - hashed_password: str - is_active: bool = True - is_superuser: bool = False \ No newline at end of file +from sqlalchemy import String, Integer, text, Boolean +from sqlalchemy.dialects.postgresql import TIMESTAMP +from sqlalchemy.orm import Mapped, mapped_column, declarative_base +from sqlalchemy.sql.schema import Column + +Base = declarative_base() + + +class User(Base): + __tablename__ = "users" # Изменяем на множественное число + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + username: Mapped[str] = mapped_column(String(50), nullable=False) + name: Mapped[str] = mapped_column(String(50), nullable=False) + email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + created_at = Column(TIMESTAMP(timezone=True), server_default=text('now()'), nullable=False) + password_hash: Mapped[str] = mapped_column("password_hash", String(length=1024), + nullable=False) # Сопоставление с БД + salt: Mapped[str] = mapped_column(String(64), nullable=False) + + # @property + # def is_active(self) -> bool: + # return True # Все пользователи активны + # + # @property + # def is_superuser(self) -> bool: + # return False # Нет суперпользователей + # + # @property + # def is_verified(self) -> bool: + # return True # Считаем всех пользователей верифицированными diff --git a/backend/profile/routers/profile.py b/backend/profile/routers/profile.py index 500c7eb6..36011766 100644 --- a/backend/profile/routers/profile.py +++ b/backend/profile/routers/profile.py @@ -1,54 +1,60 @@ from uuid import UUID from fastapi import APIRouter, HTTPException, Depends -from backend.auth.db import db, user_db -from backend.profile.schemas import UserProfile, UpdateName, UpdateEmail, UpdatePassword -from backend.profile.utils import get_current_user_id +from backend.profile.database.db import get_user_db +from backend.profile.database.postgres_users import PostgreSQLUserDatabase + router = APIRouter(prefix="/api/profile", tags=["profile"]) @router.get("/{id}") -async def get_profile(id: str): +async def get_profile(id: int, user_db: PostgreSQLUserDatabase = Depends(get_user_db)): try: - user = await user_db.get(UUID(id)) - except ValueError: - raise HTTPException(status_code=400, detail="Invalid UUID format") - if not user: - raise HTTPException(status_code=404, detail="User not found") - return user - -@router.patch("/{id}/name") -async def update_name(id: str, body: UpdateName): # user_id: str = Depends(get_current_user_id)): - # if id != user_id: - # raise HTTPException(status_code=403, detail="You can only modify your own profile") - user = await user_db.get(UUID(id)) - if not user: - raise HTTPException(status_code=404, detail="User not found") - - user.name = body.name - await user_db.update(user) - return {"status": "name updated"} - -@router.patch("/{id}/email") -async def update_email(id: str, body: UpdateEmail): - user = await user_db.get(UUID(id)) + user = await user_db.get(id) + except Exception as e: + print(f"Error getting user: {str(e)}") + raise HTTPException(status_code=400, detail=str(e)) if not user: raise HTTPException(status_code=404, detail="User not found") - - user.email = body.email - await user_db.update(user) - return {"status": "email updated"} - -@router.patch("/{id}/password") -async def update_password(id: str, body: UpdatePassword): - # fastapi-users UserManager - # return {"status": "password updated"} - raise HTTPException(status_code=400, detail="Password not supported") - -@router.delete("/{id}") -async def delete_profile(id: str): - user = await user_db.get(UUID(id)) - if not user: - raise HTTPException(status_code=404, detail="User not found") - - await user_db.delete(user) - return {"status": "deleted"} \ No newline at end of file + return { + "id": user.id, + "username": user.username, + "name": user.name, + "email": user.email + } + +# @router.patch("/{id}/name") +# async def update_name(id: int, body: UpdateName): # user_id: str = Depends(get_current_user_id)): +# # if id != user_id: +# # raise HTTPException(status_code=403, detail="You can only modify your own profile") +# user = await user_db.get(id) +# if not user: +# raise HTTPException(status_code=404, detail="User not found") +# +# user.name = body.name +# await user_db.update(user) +# return {"status": "name updated"} +# +# @router.patch("/{id}/email") +# async def update_email(id: int, body: UpdateEmail): +# user = await user_db.get(id) +# if not user: +# raise HTTPException(status_code=404, detail="User not found") +# +# user.email = body.email +# await user_db.update(user) +# return {"status": "email updated"} +# +# @router.patch("/{id}/password") +# async def update_password(id: int, body: UpdatePassword): +# # fastapi-users UserManager +# # return {"status": "password updated"} +# raise HTTPException(status_code=400, detail="Password not supported") +# +# @router.delete("/{id}") +# async def delete_profile(id: int): +# user = await user_db.get(id) +# if not user: +# raise HTTPException(status_code=404, detail="User not found") +# +# await user_db.delete(user) +# return {"status": "deleted"} \ No newline at end of file diff --git a/backend/profile/routers/project.py b/backend/profile/routers/project.py index 7291d79e..840148ae 100644 --- a/backend/profile/routers/project.py +++ b/backend/profile/routers/project.py @@ -1,50 +1,50 @@ -from fastapi import APIRouter -from backend.profile.db import db -from backend.profile.schemas import Project - -router = APIRouter(prefix="/api/profile/{id}/project", tags=["projects"]) - -@router.post("") -async def create_project(id: str, project: Project): - await db.projects.insert_one({**project.model_dump(), "owner_id": id}) - return {"status": "project created"} - -@router.get("") -async def get_all_projects(id: str): - projects = await db.projects.find({"owner_id": id}).to_list(100) - return projects - -@router.get("/{pid}") -async def get_project(id: str, pid: str): - project = await db.projects.find_one({"owner_id": id, "pid": pid}) - return project - -@router.get("/{pid}/name") -async def get_project_name(id: str, pid: str): - proj = await db.projects.find_one({"owner_id": id, "pid": pid}, {"name": 1}) - return {"name": proj["name"]} - -@router.get("/{pid}/date-created") -async def get_project_date(id: str, pid: str): - proj = await db.projects.find_one({"owner_id": id, "pid": pid}, {"date_created": 1}) - return {"date_created": proj["date_created"]} - -@router.patch("/{pid}/name") -async def update_project_name(id: str, pid: str, data: dict): - await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"name": data["name"]}}) - return {"status": "name updated"} - -@router.patch("/{pid}/circuit") -async def update_project_circuit(id: str, pid: str, data: dict): - await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"circuit": data}}) - return {"status": "circuit updated"} - -@router.patch("/{pid}/verilog") -async def update_project_verilog(id: str, pid: str, data: dict): - await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"verilog": data["verilog"]}}) - return {"status": "verilog updated"} - -@router.delete("/{pid}") -async def delete_project(id: str, pid: str): - await db.projects.delete_one({"owner_id": id, "pid": pid}) - return {"status": "project deleted"} \ No newline at end of file +# from fastapi import APIRouter +# from backend.profile.database.db import get_user_db +# from backend.profile.schemas import Project +# +# router = APIRouter(prefix="/api/profile/{id}/project", tags=["projects"]) +# +# @router.post("") +# async def create_project(id: str, project: Project): +# await db.projects.insert_one({**project.model_dump(), "owner_id": id}) +# return {"status": "project created"} +# +# @router.get("") +# async def get_all_projects(id: str): +# projects = await db.projects.find({"owner_id": id}).to_list(100) +# return projects +# +# @router.get("/{pid}") +# async def get_project(id: str, pid: str): +# project = await db.projects.find_one({"owner_id": id, "pid": pid}) +# return project +# +# @router.get("/{pid}/name") +# async def get_project_name(id: str, pid: str): +# proj = await db.projects.find_one({"owner_id": id, "pid": pid}, {"name": 1}) +# return {"name": proj["name"]} +# +# @router.get("/{pid}/date-created") +# async def get_project_date(id: str, pid: str): +# proj = await db.projects.find_one({"owner_id": id, "pid": pid}, {"date_created": 1}) +# return {"date_created": proj["date_created"]} +# +# @router.patch("/{pid}/name") +# async def update_project_name(id: str, pid: str, data: dict): +# await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"name": data["name"]}}) +# return {"status": "name updated"} +# +# @router.patch("/{pid}/circuit") +# async def update_project_circuit(id: str, pid: str, data: dict): +# await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"circuit": data}}) +# return {"status": "circuit updated"} +# +# @router.patch("/{pid}/verilog") +# async def update_project_verilog(id: str, pid: str, data: dict): +# await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"verilog": data["verilog"]}}) +# return {"status": "verilog updated"} +# +# @router.delete("/{pid}") +# async def delete_project(id: str, pid: str): +# await db.projects.delete_one({"owner_id": id, "pid": pid}) +# return {"status": "project deleted"} \ No newline at end of file diff --git a/backend/profile/schemas.py b/backend/profile/schemas.py index 473a4c84..7e431598 100644 --- a/backend/profile/schemas.py +++ b/backend/profile/schemas.py @@ -1,12 +1,38 @@ from pydantic import EmailStr +from datetime import datetime from typing import Optional from uuid import UUID -from fastapi_users import schemas +from fastapi_users import schemas, models -class UserProfile(schemas.BaseModel): - id: UUID +class UserProfile(schemas.BaseUser): + id: int + username: str name: str email: EmailStr + created_at: str + +class UserRead(schemas.BaseUser[int]): + username: str + name: str + email: EmailStr + created_at: datetime + +class UserCreate(schemas.BaseUserCreate): + username: str + name: str + email: EmailStr + password: str + +class UserUpdate(schemas.BaseUserUpdate): + name: Optional[str] = None + email: Optional[EmailStr] = None + password: Optional[str] = None + +class UserDB(UserRead, schemas.BaseUser): + salt: str + + class Config: + orm_mode = True class UpdateName(schemas.BaseModel): name: str diff --git a/backend/profile/utils.py b/backend/profile/utils.py index 86ff0d0c..86bc95a0 100644 --- a/backend/profile/utils.py +++ b/backend/profile/utils.py @@ -1,3 +1,6 @@ +import base64 +import json + from fastapi import Depends, HTTPException, Header import httpx @@ -13,4 +16,14 @@ async def get_current_user_id(authorization: str = Header(...)) -> str: if response.status_code != 200: raise HTTPException(status_code=401, detail="Invalid or expired token") - return response.json().get("user_id") \ No newline at end of file + return response.json().get("user_id") + +def decode_jwt(token: str): + try: + payload_part = token.split('.')[1] + padded = payload_part + '=' * (-len(payload_part) % 4) + decoded_bytes = base64.urlsafe_b64decode(padded) + payload = json.loads(decoded_bytes) + return payload + except Exception as e: + raise ValueError(f"Invalid token format: {e}") \ No newline at end of file diff --git a/backend/profile/verify.py b/backend/profile/verify.py deleted file mode 100644 index 6f11f31d..00000000 --- a/backend/profile/verify.py +++ /dev/null @@ -1,24 +0,0 @@ -# from uuid import UUID -# -# from fastapi import APIRouter, Depends -# from fastapi_users import FastAPIUsers -# -# from backend.profile.models import UserDB -# -# temp_router = APIRouter() -# -# fastapi_users = FastAPIUsers[UserDB, UUID]( -# get_user_manager, -# [auth_backend, refresh_backend], -# ) -# -# get_current_user = fastapi_users.current_user(active=False, verified=False) -# -# @temp_router.get("/verify", tags=["auth"]) -# async def verify_access_token(user: UserDB = Depends(get_current_user)): -# return { -# "valid": True, -# "user_id": str(user.id), -# "email": user.email, -# } -# diff --git a/backend/tests/profile_tests/conftest.py b/backend/tests/profile_tests/conftest.py index 4d2fd99a..3e7567f4 100644 --- a/backend/tests/profile_tests/conftest.py +++ b/backend/tests/profile_tests/conftest.py @@ -1,3 +1,5 @@ +import os + import httpx import pytest_asyncio @@ -8,20 +10,21 @@ @pytest_asyncio.fixture async def auth_client(): - transport = httpx.ASGITransport(app=auth_app) - return httpx.AsyncClient(transport=transport, base_url="http://test") + auth_url = os.getenv("AUTH_SERVICE_URL", "http://localhost:8080") + async with httpx.AsyncClient(base_url=auth_url) as client: + yield client @pytest_asyncio.fixture async def profile_client(): transport = httpx.ASGITransport(app=profile_app) return httpx.AsyncClient(transport=transport, base_url="http://test") -@pytest_asyncio.fixture(autouse=True) -async def cleanup(auth_client): - await user_collection.delete_many({}) - yield - await user_collection.delete_many({}) - await auth_client.aclose() +# @pytest_asyncio.fixture(autouse=True) +# async def cleanup(auth_client): +# await user_collection.delete_many({}) +# yield +# await user_collection.delete_many({}) +# await auth_client.aclose() @pytest_asyncio.fixture async def registered_user(auth_client): @@ -31,7 +34,7 @@ async def registered_user(auth_client): "email": "test@example.com", "password": "TestPassword123" } - response = await auth_client.post("/auth/register", json=user_data) - assert response.status_code == 201 + response = await auth_client.post("/api/auth/register", json=user_data) + assert response.status_code == 201 or response.status_code == 409 - return user_data, response.json() \ No newline at end of file + return user_data \ No newline at end of file diff --git a/backend/tests/profile_tests/test_get_info.py b/backend/tests/profile_tests/test_get_info.py index db2d7e2e..f4a5c98a 100644 --- a/backend/tests/profile_tests/test_get_info.py +++ b/backend/tests/profile_tests/test_get_info.py @@ -1,28 +1,28 @@ import pytest from fastapi import status +from backend.profile.utils import decode_jwt @pytest.mark.asyncio async def test_get_info(auth_client, registered_user, profile_client): - user_data, user = registered_user + user_data = registered_user login_response = await auth_client.post( - "/auth/login", - data={ - "username": user_data["email"], + "/api/auth/login", + json={ + "login": user_data["username"], "password": user_data["password"] } ) - token = login_response.json().get("access_token") + token = login_response.json().get("access") assert login_response.status_code == status.HTTP_200_OK - verify_response = await auth_client.get( - "/auth/verify", + verify_response = await auth_client.post( + "/api/auth/verify", headers={"Authorization": f"Bearer {token}"} ) assert verify_response.status_code == status.HTTP_200_OK - id = verify_response.json()["user_id"] - assert verify_response.json()["email"] == user_data["email"] + id = decode_jwt(token)['id'] response = await profile_client.get( f"/api/profile/{id}", diff --git a/backend/tests/profile_tests/test_update_email.py b/backend/tests/profile_tests/test_update_email.py index cd3d88c7..1c91b054 100644 --- a/backend/tests/profile_tests/test_update_email.py +++ b/backend/tests/profile_tests/test_update_email.py @@ -7,7 +7,7 @@ async def test_get_info(auth_client, registered_user, profile_client): user_data, user = registered_user login_response = await auth_client.post( - "/auth/login", + "http://auth:8080/api/auth/login", data={ "username": user_data["email"], "password": user_data["password"] diff --git a/docker-compose.yml b/docker-compose.yml index fca9675f..58e3d085 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ services: postgres: image: postgres:17-alpine + ports: + - "5432:5432" environment: POSTGRES_DB: "vcd" POSTGRES_USER: "vcd" @@ -11,6 +13,8 @@ services: build: ./RunnerNode auth: build: ./auth + ports: + - "8080:8080" environment: POSTGRES_DB: "vcd" POSTGRES_USER: "vcd" From 47404fce4ce8f136bb2b6afd9a88e5b7fe5f1111 Mon Sep 17 00:00:00 2001 From: doshq Date: Fri, 18 Jul 2025 12:59:17 +0300 Subject: [PATCH 079/152] Add project handlers. Unit tests for creating, getting projects. Fixed update email handler --- backend/profile/database/db.py | 12 +- backend/profile/database/pgre_projects.py | 69 +++++++++ backend/profile/database/postgres_users.py | 4 +- backend/profile/main.py | 2 +- backend/profile/models.py | 22 ++- backend/profile/routers/profile.py | 68 ++++----- backend/profile/routers/project.py | 140 +++++++++++------- backend/profile/schemas.py | 22 ++- backend/tests/profile_tests/conftest.py | 68 +++++++-- .../profile_tests/test_create_project.py | 18 +++ backend/tests/profile_tests/test_get_info.py | 12 +- .../tests/profile_tests/test_get_projects.py | 18 +++ .../tests/profile_tests/test_update_email.py | 27 +--- .../tests/profile_tests/test_update_name.py | 28 ++++ postgres-init/users.sql | 14 +- 15 files changed, 379 insertions(+), 145 deletions(-) create mode 100644 backend/profile/database/pgre_projects.py create mode 100644 backend/tests/profile_tests/test_create_project.py create mode 100644 backend/tests/profile_tests/test_get_projects.py create mode 100644 backend/tests/profile_tests/test_update_name.py diff --git a/backend/profile/database/db.py b/backend/profile/database/db.py index 932eb04c..9f43e5f4 100644 --- a/backend/profile/database/db.py +++ b/backend/profile/database/db.py @@ -4,9 +4,10 @@ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession from sqlalchemy.ext.asyncio.session import AsyncSession +from backend.profile.database.pgre_projects import PostgreSQLProjectDatabase from backend.profile.database.postgres_users import PostgreSQLUserDatabase -from backend.profile.models import User -from backend.profile.schemas import UserDB, UserProfile +from backend.profile.models import User, ProjectModel +from backend.profile.schemas import UserDB, UserProfile, ProjectDB try: DATABASE_URL = ( @@ -37,4 +38,11 @@ async def get_user_db(session: AsyncSession = Depends(get_async_session)): session=session, user_model=User, # ваша SQLAlchemy модель user_db_model=UserDB # ваша Pydantic схема + ) + +async def get_project_db(session: AsyncSession = Depends(get_async_session)): + yield PostgreSQLProjectDatabase( + session=session, + project_model=ProjectModel, + project_db_model=ProjectDB ) \ No newline at end of file diff --git a/backend/profile/database/pgre_projects.py b/backend/profile/database/pgre_projects.py new file mode 100644 index 00000000..61f1c5db --- /dev/null +++ b/backend/profile/database/pgre_projects.py @@ -0,0 +1,69 @@ +from typing import Any, Dict, Optional, Type, TypeVar +from fastapi_users.db.base import BaseUserDatabase +from sqlalchemy import select, delete, update, insert +from sqlalchemy.ext.asyncio import AsyncSession +from backend.profile.models import ProjectModel +from backend.profile.schemas import ProjectDB + +UP = TypeVar("UP", bound=ProjectDB) + +class PostgreSQLProjectDatabase: + def __init__( + self, + session: AsyncSession, + project_model: Type[ProjectModel], + project_db_model: Type[UP] + ): + self.session = session + self.project_model = project_model + self.project_db_model = project_db_model + + async def create(self, project_dict: Dict[str, Any]) -> UP: + stmt = insert(self.project_model).values(**project_dict).returning(self.project_model) + result = await self.session.execute(stmt) + await self.session.commit() + project = result.scalar_one() + return self._convert_to_projectdb(project) + + async def get(self, owner_id: int, pid: int) -> Optional[UP]: + stmt = select(self.project_model).where( + self.project_model.owner_id == owner_id, + self.project_model.pid == pid + ) + result = await self.session.execute(stmt) + project = result.scalar_one_or_none() + if project is None: + return None + return self._convert_to_projectdb(project) + + async def get_all(self, owner_id: int) -> list[UP]: + stmt = select(self.project_model).where(self.project_model.owner_id == owner_id) + result = await self.session.execute(stmt) + projects = result.scalars().all() + return [self._convert_to_projectdb(p) for p in projects] + + async def update(self, owner_id: int, pid: int, update_dict: Dict[str, Any]) -> UP: + stmt = ( + update(self.project_model) + .where( + self.project_model.owner_id == owner_id, + self.project_model.pid == pid + ) + .values(**update_dict) + .returning(self.project_model) + ) + result = await self.session.execute(stmt) + await self.session.commit() + project = result.scalar_one() + return self._convert_to_projectdb(project) + + async def delete(self, owner_id: int, pid: int) -> None: + stmt = delete(self.project_model).where( + self.project_model.owner_id == owner_id, + self.project_model.pid == pid + ) + await self.session.execute(stmt) + await self.session.commit() + + def _convert_to_projectdb(self, project: ProjectModel) -> UP: + return self.project_db_model(**project.__dict__) \ No newline at end of file diff --git a/backend/profile/database/postgres_users.py b/backend/profile/database/postgres_users.py index ed061d32..63666c5e 100644 --- a/backend/profile/database/postgres_users.py +++ b/backend/profile/database/postgres_users.py @@ -1,5 +1,6 @@ from typing import Any, Dict, Optional, Type, TypeVar from fastapi_users.db.base import BaseUserDatabase +from pydantic import EmailStr from sqlalchemy import select, delete, update, insert from sqlalchemy.ext.asyncio import AsyncSession from backend.profile.models import User as UserModel # SQLAlchemy модель @@ -44,7 +45,6 @@ async def get_by_username(self, username: str) -> Optional[UP]: return self._convert_to_userdb(user) async def create(self, user_dict: Dict[str, Any]) -> UP: - # Удаляем ID если он есть, так как он генерируется базой user_dict.pop("id", None) stmt = insert(self.user_model).values(**user_dict).returning(self.user_model) @@ -56,7 +56,7 @@ async def create(self, user_dict: Dict[str, Any]) -> UP: async def update( self, user: UP, - update_dict: Dict[str, Any] + update_dict: Dict[str, EmailStr | str] ) -> UP: stmt = ( update(self.user_model) diff --git a/backend/profile/main.py b/backend/profile/main.py index dcafbc25..c9d01fd0 100644 --- a/backend/profile/main.py +++ b/backend/profile/main.py @@ -5,4 +5,4 @@ app = FastAPI() app.include_router(profile.router) -# app.include_router(project.router) +app.include_router(project.router) diff --git a/backend/profile/models.py b/backend/profile/models.py index a15e70d2..ef10940d 100644 --- a/backend/profile/models.py +++ b/backend/profile/models.py @@ -1,7 +1,7 @@ -from sqlalchemy import String, Integer, text, Boolean +from sqlalchemy import String, Integer, text, Boolean, JSON from sqlalchemy.dialects.postgresql import TIMESTAMP from sqlalchemy.orm import Mapped, mapped_column, declarative_base -from sqlalchemy.sql.schema import Column +from sqlalchemy.sql.schema import Column, ForeignKey Base = declarative_base() @@ -20,12 +20,24 @@ class User(Base): # @property # def is_active(self) -> bool: - # return True # Все пользователи активны + # return True # # @property # def is_superuser(self) -> bool: - # return False # Нет суперпользователей + # return False # # @property # def is_verified(self) -> bool: - # return True # Считаем всех пользователей верифицированными + # return True + + +class ProjectModel(Base): + __tablename__ = "projects" + + pid: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + owner_id: Mapped[int] = mapped_column(ForeignKey("user.id")) + name: Mapped[str] = mapped_column(String(100)) + circuit: Mapped[dict] = mapped_column(JSON, nullable=True) + custom_nodes: Mapped[dict] = mapped_column(JSON, nullable=True) + verilog: Mapped[dict] = mapped_column(String(10000), nullable=True) + created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True), server_default=text("now()")) diff --git a/backend/profile/routers/profile.py b/backend/profile/routers/profile.py index 36011766..36d57acf 100644 --- a/backend/profile/routers/profile.py +++ b/backend/profile/routers/profile.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, HTTPException, Depends from backend.profile.database.db import get_user_db from backend.profile.database.postgres_users import PostgreSQLUserDatabase - +from backend.profile.schemas import UpdateName, UpdateEmail, UpdatePassword router = APIRouter(prefix="/api/profile", tags=["profile"]) @@ -22,39 +22,39 @@ async def get_profile(id: int, user_db: PostgreSQLUserDatabase = Depends(get_use "email": user.email } -# @router.patch("/{id}/name") -# async def update_name(id: int, body: UpdateName): # user_id: str = Depends(get_current_user_id)): -# # if id != user_id: -# # raise HTTPException(status_code=403, detail="You can only modify your own profile") -# user = await user_db.get(id) -# if not user: -# raise HTTPException(status_code=404, detail="User not found") -# -# user.name = body.name -# await user_db.update(user) -# return {"status": "name updated"} -# -# @router.patch("/{id}/email") -# async def update_email(id: int, body: UpdateEmail): -# user = await user_db.get(id) -# if not user: -# raise HTTPException(status_code=404, detail="User not found") -# -# user.email = body.email -# await user_db.update(user) -# return {"status": "email updated"} +@router.patch("/{id}/name") +async def update_name(id: int, body: UpdateName, user_db: PostgreSQLUserDatabase = Depends(get_user_db)): # user_id: str = Depends(get_current_user_id)): + # if id != user_id: + # raise HTTPException(status_code=403, detail="You can only modify your own profile") + user = await user_db.get(id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + update_data = {"name": body.name} + await user_db.update(user, update_dict=update_data) + return {"status": "name updated"} # -# @router.patch("/{id}/password") -# async def update_password(id: int, body: UpdatePassword): -# # fastapi-users UserManager -# # return {"status": "password updated"} -# raise HTTPException(status_code=400, detail="Password not supported") +@router.patch("/{id}/email") +async def update_email(id: int, body: UpdateEmail, user_db: PostgreSQLUserDatabase = Depends(get_user_db)): + user = await user_db.get(id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + update_data = {"email": body.email} + await user_db.update(user, update_dict=update_data) + return {"status": "email updated"} # -# @router.delete("/{id}") -# async def delete_profile(id: int): -# user = await user_db.get(id) -# if not user: -# raise HTTPException(status_code=404, detail="User not found") +@router.patch("/{id}/password") +async def update_password(id: int, body: UpdatePassword, user_db: PostgreSQLUserDatabase = Depends(get_user_db)): + # fastapi-users UserManager + # return {"status": "password updated"} + raise HTTPException(status_code=400, detail="Password not supported") # -# await user_db.delete(user) -# return {"status": "deleted"} \ No newline at end of file +@router.delete("/{id}") +async def delete_profile(id: int, user_db: PostgreSQLUserDatabase = Depends(get_user_db)): + user = await user_db.get(id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + await user_db.delete(user) + return {"status": "deleted"} \ No newline at end of file diff --git a/backend/profile/routers/project.py b/backend/profile/routers/project.py index 840148ae..f43a526d 100644 --- a/backend/profile/routers/project.py +++ b/backend/profile/routers/project.py @@ -1,50 +1,90 @@ -# from fastapi import APIRouter -# from backend.profile.database.db import get_user_db -# from backend.profile.schemas import Project -# -# router = APIRouter(prefix="/api/profile/{id}/project", tags=["projects"]) -# -# @router.post("") -# async def create_project(id: str, project: Project): -# await db.projects.insert_one({**project.model_dump(), "owner_id": id}) -# return {"status": "project created"} -# -# @router.get("") -# async def get_all_projects(id: str): -# projects = await db.projects.find({"owner_id": id}).to_list(100) -# return projects -# -# @router.get("/{pid}") -# async def get_project(id: str, pid: str): -# project = await db.projects.find_one({"owner_id": id, "pid": pid}) -# return project -# -# @router.get("/{pid}/name") -# async def get_project_name(id: str, pid: str): -# proj = await db.projects.find_one({"owner_id": id, "pid": pid}, {"name": 1}) -# return {"name": proj["name"]} -# -# @router.get("/{pid}/date-created") -# async def get_project_date(id: str, pid: str): -# proj = await db.projects.find_one({"owner_id": id, "pid": pid}, {"date_created": 1}) -# return {"date_created": proj["date_created"]} -# -# @router.patch("/{pid}/name") -# async def update_project_name(id: str, pid: str, data: dict): -# await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"name": data["name"]}}) -# return {"status": "name updated"} -# -# @router.patch("/{pid}/circuit") -# async def update_project_circuit(id: str, pid: str, data: dict): -# await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"circuit": data}}) -# return {"status": "circuit updated"} -# -# @router.patch("/{pid}/verilog") -# async def update_project_verilog(id: str, pid: str, data: dict): -# await db.projects.update_one({"owner_id": id, "pid": pid}, {"$set": {"verilog": data["verilog"]}}) -# return {"status": "verilog updated"} -# -# @router.delete("/{pid}") -# async def delete_project(id: str, pid: str): -# await db.projects.delete_one({"owner_id": id, "pid": pid}) -# return {"status": "project deleted"} \ No newline at end of file +from fastapi import APIRouter, Depends, HTTPException +from backend.profile.database.db import get_project_db +from backend.profile.database.pgre_projects import PostgreSQLProjectDatabase +from backend.profile.schemas import Project, ProjectDB + +router = APIRouter(prefix="/api/profile/{id}/project", tags=["projects"]) + + +@router.post("", response_model=dict) +async def create_project( + id: int, + project: Project, + project_db: PostgreSQLProjectDatabase = Depends(get_project_db) +): + project_dict = project.model_dump() + project_dict["owner_id"] = id + await project_db.create(project_dict) + return {"status": "project created"} + + +@router.get("", response_model=list[ProjectDB]) +async def get_all_projects( + id: int, + project_db: PostgreSQLProjectDatabase = Depends(get_project_db) +): + return await project_db.get_all(id) + + +@router.get("/{pid}", response_model=ProjectDB) +async def get_project( + id: int, + pid: int, + project_db: PostgreSQLProjectDatabase = Depends(get_project_db) +): + project = await project_db.get(id, pid) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + return project + + +@router.patch("/{pid}/name", response_model=dict) +async def update_project_name( + id: int, + pid: int, + data: dict, + project_db: PostgreSQLProjectDatabase = Depends(get_project_db) +): + if "name" not in data: + raise HTTPException(status_code=400, detail="Name is required") + + await project_db.update(id, pid, {"name": data["name"]}) + return {"status": "name updated"} + + +@router.patch("/{pid}/circuit", response_model=dict) +async def update_project_circuit( + id: int, + pid: int, + data: dict, + project_db: PostgreSQLProjectDatabase = Depends(get_project_db) +): + if "circuit" not in data: + raise HTTPException(status_code=400, detail="Circuit data is required") + + await project_db.update(id, pid, {"circuit": data["circuit"]}) + return {"status": "circuit updated"} + + +@router.patch("/{pid}/custom_nodes", response_model=dict) +async def update_custom_nodes( + id: int, + pid: int, + data: dict, + project_db: PostgreSQLProjectDatabase = Depends(get_project_db) +): + if "custom_nodes" not in data: + raise HTTPException(status_code=400, detail="Custom nodes data is required") + + await project_db.update(id, pid, {"custom_nodes": data["custom_nodes"]}) + return {"status": "custom_nodes updated"} + + +@router.delete("/{pid}", response_model=dict) +async def delete_project( + id: int, + pid: int, + project_db: PostgreSQLProjectDatabase = Depends(get_project_db) +): + await project_db.delete(id, pid) + return {"status": "project deleted"} \ No newline at end of file diff --git a/backend/profile/schemas.py b/backend/profile/schemas.py index 7e431598..fe3978c7 100644 --- a/backend/profile/schemas.py +++ b/backend/profile/schemas.py @@ -1,9 +1,11 @@ from pydantic import EmailStr from datetime import datetime -from typing import Optional +from typing import Optional, Dict from uuid import UUID from fastapi_users import schemas, models + +# Users schemas class UserProfile(schemas.BaseUser): id: int username: str @@ -42,10 +44,18 @@ class UpdateEmail(schemas.BaseModel): class UpdatePassword(schemas.BaseModel): password: str - + + +# Projects schemas class Project(schemas.BaseModel): - pid: UUID name: str - date_created: str - circuit: dict - verilog: str \ No newline at end of file + circuit: Optional[Dict] = None + verilog: Optional[str] = None + custom_nodes: Optional[Dict] = None + +class ProjectDB(Project): + pid: int + owner_id: int + + class Config: + from_attributes = True \ No newline at end of file diff --git a/backend/tests/profile_tests/conftest.py b/backend/tests/profile_tests/conftest.py index 3e7567f4..2d0fae9a 100644 --- a/backend/tests/profile_tests/conftest.py +++ b/backend/tests/profile_tests/conftest.py @@ -1,11 +1,8 @@ import os - import httpx import pytest_asyncio - -from backend.auth.main import app as auth_app +from fastapi import status from backend.profile.main import app as profile_app -from backend.auth.db import user_collection @pytest_asyncio.fixture @@ -19,22 +16,67 @@ async def profile_client(): transport = httpx.ASGITransport(app=profile_app) return httpx.AsyncClient(transport=transport, base_url="http://test") -# @pytest_asyncio.fixture(autouse=True) -# async def cleanup(auth_client): -# await user_collection.delete_many({}) -# yield -# await user_collection.delete_many({}) -# await auth_client.aclose() - @pytest_asyncio.fixture async def registered_user(auth_client): user_data = { "name": "Test User", - "username": "testuser", + "username": "unittest", "email": "test@example.com", "password": "TestPassword123" } response = await auth_client.post("/api/auth/register", json=user_data) assert response.status_code == 201 or response.status_code == 409 - return user_data \ No newline at end of file + login_response = await auth_client.post( + "/api/auth/login", + json={ + "login": user_data["username"], + "password": user_data["password"] + } + ) + token = login_response.json().get("access") + assert login_response.status_code == status.HTTP_200_OK + + return user_data, token + +@pytest_asyncio.fixture +async def test_project_data(): + return { + "name": "Test Project", + "circuit": { + "nodes": [ + { + "id": "andNode_1751219192609", + "type": "andNode", + "position": { + "x": 130, + "y": 220 + }, + "data": { + "customId": "andNode_1751219192609" + } + }, + { + "id": "inputNodeSwitch_1751219193704", + "type": "inputNodeSwitch", + "position": { + "x": 10, + "y": 370 + }, + "data": { + "customId": "inputNodeSwitch_1751219193704" + } + } + ], + "edges": [ + { + "id": "xy-edge__inputNodeSwitch_1751219193704output-1-andNode_1751219192609input-1", + "source": "inputNodeSwitch_1751219193704", + "target": "andNode_1751219192609", + "sourceHandle": "output-1", + "targetHandle": "input-1" + } + ] + }, + # "custom_nodes": [] + } \ No newline at end of file diff --git a/backend/tests/profile_tests/test_create_project.py b/backend/tests/profile_tests/test_create_project.py new file mode 100644 index 00000000..11c2006c --- /dev/null +++ b/backend/tests/profile_tests/test_create_project.py @@ -0,0 +1,18 @@ +import pytest +from fastapi import status +from backend.profile.utils import decode_jwt + + +@pytest.mark.asyncio +async def test_create_project(auth_client, registered_user, profile_client, test_project_data): + user_data, token = registered_user + + id = decode_jwt(token)['id'] + + response = await profile_client.post( + f"/api/profile/{id}/project", + json=test_project_data + ) + + assert response.status_code == status.HTTP_200_OK + assert response.json() == {"status": "project created"} diff --git a/backend/tests/profile_tests/test_get_info.py b/backend/tests/profile_tests/test_get_info.py index f4a5c98a..394cec36 100644 --- a/backend/tests/profile_tests/test_get_info.py +++ b/backend/tests/profile_tests/test_get_info.py @@ -5,17 +5,7 @@ @pytest.mark.asyncio async def test_get_info(auth_client, registered_user, profile_client): - user_data = registered_user - - login_response = await auth_client.post( - "/api/auth/login", - json={ - "login": user_data["username"], - "password": user_data["password"] - } - ) - token = login_response.json().get("access") - assert login_response.status_code == status.HTTP_200_OK + user_data, token = registered_user verify_response = await auth_client.post( "/api/auth/verify", diff --git a/backend/tests/profile_tests/test_get_projects.py b/backend/tests/profile_tests/test_get_projects.py new file mode 100644 index 00000000..a1f8a177 --- /dev/null +++ b/backend/tests/profile_tests/test_get_projects.py @@ -0,0 +1,18 @@ +import pytest +from fastapi import status +from backend.profile.utils import decode_jwt + + +@pytest.mark.asyncio +async def test_create_project(auth_client, registered_user, profile_client, test_project_data): + user_data, token = registered_user + + id = decode_jwt(token)['id'] + + response = await profile_client.get( + f"/api/profile/{id}/project", + ) + + print("GET: ", response.json()) + assert response.status_code == status.HTTP_200_OK + assert response.json() diff --git a/backend/tests/profile_tests/test_update_email.py b/backend/tests/profile_tests/test_update_email.py index 1c91b054..b9ab0a8c 100644 --- a/backend/tests/profile_tests/test_update_email.py +++ b/backend/tests/profile_tests/test_update_email.py @@ -1,31 +1,17 @@ import pytest from fastapi import status +from backend.profile.utils import decode_jwt @pytest.mark.asyncio -async def test_get_info(auth_client, registered_user, profile_client): - user_data, user = registered_user - - login_response = await auth_client.post( - "http://auth:8080/api/auth/login", - data={ - "username": user_data["email"], - "password": user_data["password"] - } - ) - token = login_response.json().get("access_token") - assert login_response.status_code == status.HTTP_200_OK +async def test_update_email(auth_client, registered_user, profile_client): + user_data, token = registered_user - # verify_response = await auth_client.get( - # "/auth/verify", - # headers={"Authorization": f"Bearer {token}"} - # ) - # assert verify_response.status_code == status.HTTP_200_OK - # id = verify_response.json()["user_id"] + id = decode_jwt(token)['id'] new_email = "newemail@example.com" update_response = await profile_client.patch( - f"/api/profile/{str(user['id'])}/email", + f"/api/profile/{id}/email", json={"email": new_email}, headers={"Authorization": f"Bearer {token}"} ) @@ -34,9 +20,10 @@ async def test_get_info(auth_client, registered_user, profile_client): assert update_response.json()["status"] == "email updated" profile_response = await profile_client.get( - f"/api/profile/{str(user['id'])}", + f"/api/profile/{id}", headers={"Authorization": f"Bearer {token}"} ) assert profile_response.status_code == status.HTTP_200_OK assert profile_response.json()["email"] == new_email + diff --git a/backend/tests/profile_tests/test_update_name.py b/backend/tests/profile_tests/test_update_name.py new file mode 100644 index 00000000..d7539c62 --- /dev/null +++ b/backend/tests/profile_tests/test_update_name.py @@ -0,0 +1,28 @@ +import pytest +from fastapi import status +from backend.profile.utils import decode_jwt + + +@pytest.mark.asyncio +async def test_update_name(auth_client, registered_user, profile_client): + user_data, token = registered_user + + id = decode_jwt(token)['id'] + + new_name = "Mikhail Viktorovich" + update_response = await profile_client.patch( + f"/api/profile/{id}/name", + json={"name": new_name}, + headers={"Authorization": f"Bearer {token}"} + ) + + assert update_response.status_code == status.HTTP_200_OK + assert update_response.json()["status"] == "name updated" + + profile_response = await profile_client.get( + f"/api/profile/{id}", + headers={"Authorization": f"Bearer {token}"} + ) + + assert profile_response.status_code == status.HTTP_200_OK + assert profile_response.json()["name"] == new_name \ No newline at end of file diff --git a/postgres-init/users.sql b/postgres-init/users.sql index b4073315..9eaf407e 100644 --- a/postgres-init/users.sql +++ b/postgres-init/users.sql @@ -32,4 +32,16 @@ ALTER TABLE ONLY public.users ADD CONSTRAINT users_pkey PRIMARY KEY (id); ALTER TABLE ONLY public.users - ADD CONSTRAINT users_username_key UNIQUE (username); \ No newline at end of file + ADD CONSTRAINT users_username_key UNIQUE (username); + +CREATE TABLE public.projects ( + pid SERIAL PRIMARY KEY, + owner_id INTEGER NOT NULL REFERENCES users(id), + name VARCHAR(100) NOT NULL, + circuit JSONB, + custom_nodes JSONB, + verilog TEXT, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL +); + +ALTER TABLE public.projects OWNER TO vcd; \ No newline at end of file From a1f98f0ffc56b323e68838e7648be169d0869b8e Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Fri, 18 Jul 2025 18:45:36 +0300 Subject: [PATCH 080/152] getEditableNode tests introduced --- UI/src/components/pages/mainPage.jsx | 17 +------ .../unit tests/getEditableNode.unit.test.js | 44 +++++++++++++++++++ UI/src/components/utils/getEditableNode.js | 16 +++++++ 3 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 UI/src/components/utils/__tests__/unit tests/getEditableNode.unit.test.js create mode 100644 UI/src/components/utils/getEditableNode.js diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 068b7e12..148ad09a 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -64,6 +64,7 @@ import { createHistoryUpdater } from "../utils/createHistoryUpdater.js"; import { undo as undoUtil } from "../utils/undo.js"; import { redo as redoUtil } from "../utils/redo.js"; import { handleTabSwitch as handleTabSwitchUtil } from "../utils/handleTabSwitch.js"; +import { getEditableNode} from "../utils/getEditableNode.js"; export const SimulateStateContext = createContext({ simulateState: "idle", @@ -135,21 +136,7 @@ export default function Main() { const ignoreChangesRef = useRef(false); - const editableNode = useMemo(() => { - const selectedNodes = nodes.filter((n) => n.selected); - const selectedEdges = edges.filter((e) => e.selected); - if (selectedNodes.length === 1 && selectedEdges.length === 0) { - const node = selectedNodes[0]; - if ( - ["inputNodeSwitch", "inputNodeButton", "outputNodeLed"].includes( - node.type, - ) - ) { - return node; - } - } - return null; - }, [nodes]); + const editableNode = useMemo(() => getEditableNode(nodes, edges), [nodes, edges]); const handleNameChange = (e) => { if (!editableNode) return; diff --git a/UI/src/components/utils/__tests__/unit tests/getEditableNode.unit.test.js b/UI/src/components/utils/__tests__/unit tests/getEditableNode.unit.test.js new file mode 100644 index 00000000..d2e18373 --- /dev/null +++ b/UI/src/components/utils/__tests__/unit tests/getEditableNode.unit.test.js @@ -0,0 +1,44 @@ +import { getEditableNode } from "../../getEditableNode"; + +describe("getEditableNode", () => { + const edge = (id) => ({ id, selected: true }); + + const createNode = (type, selected = true) => ({ + id: "1", + type, + selected, + }); + + it("returns the node if exactly one valid node is selected and no edge is selected", () => { + const nodes = [createNode("inputNodeSwitch")]; + const edges = []; + expect(getEditableNode(nodes, edges)).toEqual(nodes[0]); + }); + + it("returns null if no node is selected", () => { + const nodes = [{ id: "1", type: "inputNodeSwitch", selected: false }]; + const edges = []; + expect(getEditableNode(nodes, edges)).toBeNull(); + }); + + it("returns null if more than one node is selected", () => { + const nodes = [ + createNode("inputNodeSwitch"), + createNode("inputNodeButton"), + ]; + const edges = []; + expect(getEditableNode(nodes, edges)).toBeNull(); + }); + + it("returns null if a node is selected but edge is also selected", () => { + const nodes = [createNode("inputNodeSwitch")]; + const edges = [edge("e1")]; + expect(getEditableNode(nodes, edges)).toBeNull(); + }); + + it("returns null if selected node has unsupported type", () => { + const nodes = [createNode("customNodeType")]; + const edges = []; + expect(getEditableNode(nodes, edges)).toBeNull(); + }); +}); diff --git a/UI/src/components/utils/getEditableNode.js b/UI/src/components/utils/getEditableNode.js new file mode 100644 index 00000000..69bfd3ba --- /dev/null +++ b/UI/src/components/utils/getEditableNode.js @@ -0,0 +1,16 @@ +export function getEditableNode(nodes, edges) { + const selectedNodes = nodes.filter((n) => n.selected); + const selectedEdges = edges.filter((e) => e.selected); + + if (selectedNodes.length === 1 && selectedEdges.length === 0) { + const node = selectedNodes[0]; + if ( + ["inputNodeSwitch", "inputNodeButton", "outputNodeLed"].includes( + node.type, + ) + ) { + return node; + } + } + return null; +} From 19a08240fddb2b5a9aca1e85215766a2150eb8a9 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Fri, 18 Jul 2025 20:04:14 +0300 Subject: [PATCH 081/152] nodes spawn fixed --- UI/src/components/pages/mainPage.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 148ad09a..353b2ff8 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -326,7 +326,6 @@ export default function Main() { const { nodes: newNodes, edges: newEdges } = deselectAllUtil(nodes, edges); setNodes(newNodes); setEdges(newEdges); - recordHistory(); }, [nodes, edges, setNodes, setEdges]); const deleteSelectedElements = useCallback(() => { From 967820bc97e89f1831a5a10b36e48ab7ed472c1e Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Fri, 18 Jul 2025 20:11:26 +0300 Subject: [PATCH 082/152] node icons moved up a little --- UI/assets/circuits-icons.jsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/UI/assets/circuits-icons.jsx b/UI/assets/circuits-icons.jsx index 31b3941a..5350bbd7 100644 --- a/UI/assets/circuits-icons.jsx +++ b/UI/assets/circuits-icons.jsx @@ -1,5 +1,5 @@ export const IconAND = ({ SVGClassName, style }) => ( - + AND ( ); export const IconOR = ({ SVGClassName, style }) => ( - + OR ( ); export const IconNAND = ({ SVGClassName, style }) => ( - + NAND ( ); export const IconNOR = ({ SVGClassName, style }) => ( - + NOR ( ); export const IconXOR = ({ SVGClassName, style }) => ( - + XOR ( ); export const IconNOT = ({ SVGClassName, style }) => ( - + NOT ( ); export const IconInput = ({ SVGClassName, style }) => ( - + Input ( ); export const IconOutput = ({ SVGClassName, style }) => ( - + Output Date: Fri, 18 Jul 2025 23:32:24 +0300 Subject: [PATCH 083/152] if title of the tab deleted, it returns to default state --- UI/src/components/pages/mainPage/tabs.jsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/UI/src/components/pages/mainPage/tabs.jsx b/UI/src/components/pages/mainPage/tabs.jsx index 0c92f6fd..60fbe1c5 100644 --- a/UI/src/components/pages/mainPage/tabs.jsx +++ b/UI/src/components/pages/mainPage/tabs.jsx @@ -96,9 +96,13 @@ export default function TabsContainer({ setContextMenu(null); }; - const handleKeyDown = (e) => { + const handleKeyDown = (e, tabId) => { if (e.key === "Enter") { e.preventDefault(); + const tab = tabs.find((t) => t.id === tabId); + if (tab && tab.title.trim() === "") { + updateTabTitle(tabId, "Untitled Tab"); + } setEditingTabId(null); } if (e.key === "Escape") { @@ -106,7 +110,14 @@ export default function TabsContainer({ } }; + const handleBlur = () => { + if (editingTabId !== null) { + const tab = tabs.find((t) => t.id === editingTabId); + if (tab && tab.title.trim() === "") { + updateTabTitle(editingTabId, "Untitled Tab"); + } + } setEditingTabId(null); }; From c203c4bcc593bd51e7ef3416f4a59f6783a808b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:32:53 +0000 Subject: [PATCH 084/152] Automated formatting --- UI/src/components/pages/mainPage.jsx | 7 +++++-- UI/src/components/pages/mainPage/tabs.jsx | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 353b2ff8..a2784512 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -64,7 +64,7 @@ import { createHistoryUpdater } from "../utils/createHistoryUpdater.js"; import { undo as undoUtil } from "../utils/undo.js"; import { redo as redoUtil } from "../utils/redo.js"; import { handleTabSwitch as handleTabSwitchUtil } from "../utils/handleTabSwitch.js"; -import { getEditableNode} from "../utils/getEditableNode.js"; +import { getEditableNode } from "../utils/getEditableNode.js"; export const SimulateStateContext = createContext({ simulateState: "idle", @@ -136,7 +136,10 @@ export default function Main() { const ignoreChangesRef = useRef(false); - const editableNode = useMemo(() => getEditableNode(nodes, edges), [nodes, edges]); + const editableNode = useMemo( + () => getEditableNode(nodes, edges), + [nodes, edges], + ); const handleNameChange = (e) => { if (!editableNode) return; diff --git a/UI/src/components/pages/mainPage/tabs.jsx b/UI/src/components/pages/mainPage/tabs.jsx index 60fbe1c5..7c252a8d 100644 --- a/UI/src/components/pages/mainPage/tabs.jsx +++ b/UI/src/components/pages/mainPage/tabs.jsx @@ -110,7 +110,6 @@ export default function TabsContainer({ } }; - const handleBlur = () => { if (editingTabId !== null) { const tab = tabs.find((t) => t.id === editingTabId); From 12b13dffeceff9f3b2c43bf020108f2cc805274b Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Fri, 18 Jul 2025 23:53:15 +0300 Subject: [PATCH 085/152] handleNameChange tests introduced --- UI/src/components/pages/mainPage.jsx | 13 ++--- .../unit tests/handleNameChange.unit.test.js | 47 +++++++++++++++++++ UI/src/components/utils/handleNameChange.js | 13 +++++ 3 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 UI/src/components/utils/__tests__/unit tests/handleNameChange.unit.test.js create mode 100644 UI/src/components/utils/handleNameChange.js diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index a2784512..1e8bab35 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -65,6 +65,7 @@ import { undo as undoUtil } from "../utils/undo.js"; import { redo as redoUtil } from "../utils/redo.js"; import { handleTabSwitch as handleTabSwitchUtil } from "../utils/handleTabSwitch.js"; import { getEditableNode } from "../utils/getEditableNode.js"; +import { handleNameChange } from "../utils/handleNameChange.js"; export const SimulateStateContext = createContext({ simulateState: "idle", @@ -141,15 +142,7 @@ export default function Main() { [nodes, edges], ); - const handleNameChange = (e) => { - if (!editableNode) return; - - const newName = e.target.value; - setNodes((nds) => - nds.map((n) => (n.id === editableNode.id ? { ...n, name: newName } : n)), - ); - setTimeout(recordHistory, 0); - }; + const onNameChange = (e) => handleNameChange(e, editableNode, setNodes, recordHistory); const handleOpenClick = () => { if (fileInputRef.current) { @@ -650,7 +643,7 @@ export default function Main() {

Button

+ + + + + + ); +} + +const SvgButton = ({ pressed, onPressDown, onPressUp, disabled }) => { + return ( +
+
+
+ ); +}; + +export default InputNodeButton; diff --git a/UI/src/components/codeComponents/logger.jsx b/UI/src/components/codeComponents/logger.jsx index cd59bc15..dbe0fc81 100644 --- a/UI/src/components/codeComponents/logger.jsx +++ b/UI/src/components/codeComponents/logger.jsx @@ -10,14 +10,6 @@ export const LOG_LEVELS = { // Уровень логирования по умолчанию let currentLogLevel = LOG_LEVELS.ERROR; -export function setCurrentLogLevel(logLevel) { - currentLogLevel = logLevel; -} - -export function getCurrentLogLevel() { - return currentLogLevel; -} - export const logMessage = (msg, level = LOG_LEVELS.IMPORTANT) => { if (level <= currentLogLevel) { const levelLabel = Object.keys(LOG_LEVELS).find( @@ -49,10 +41,10 @@ export const showToastError = (msg) => {
{msg}
), - { duration: "1000" }, + { duration: "500" }, ); }; diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 068b7e12..6525a8bd 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -64,6 +64,7 @@ import { createHistoryUpdater } from "../utils/createHistoryUpdater.js"; import { undo as undoUtil } from "../utils/undo.js"; import { redo as redoUtil } from "../utils/redo.js"; import { handleTabSwitch as handleTabSwitchUtil } from "../utils/handleTabSwitch.js"; +import CreateCustomBlock from "./mainPage/customCircuit.jsx"; export const SimulateStateContext = createContext({ simulateState: "idle", @@ -700,7 +701,7 @@ export default function Main() { }, duration: 2000, error: { - duration: 10000, + duration: 1000, }, warning: { className: "toast-warning", @@ -744,6 +745,13 @@ export default function Main() { onClick={() => closeMenu()} /> + {console.log("создать из файла")}} + onCreateFromCurrent={() => {console.log("создать из текущего")}} + /> + { + return ( +
+
+ {inputs.map((input, index) => ( +
+ ))} +
+
+
+ {outputs.map((output, index) => ( +
+ ))} +
+
+ ); +}; export default function CircuitsMenu({ - circuitsMenuState, - onDragStart, - spawnCircuit, -}) { + circuitsMenuState, + onDragStart, + spawnCircuit, + }) { const [openIndexes, setOpenIndexes] = useState([]); + const [customBlocks, setCustomBlocks] = useState([]); + + // Загружаем кастомные блоки при монтировании + useEffect(() => { + const blocks = loadCustomBlocks(); + setCustomBlocks(blocks); + }, []); + + // Обработчик для обновления списка кастомных блоков + const refreshCustomBlocks = useCallback(() => { + const blocks = loadCustomBlocks(); + setCustomBlocks(blocks); + }, []); const toggleItem = useCallback((index) => { setOpenIndexes((prevIndexes) => - prevIndexes.includes(index) - ? prevIndexes.filter((i) => i !== index) - : [...prevIndexes, index], + prevIndexes.includes(index) + ? prevIndexes.filter((i) => i !== index) + : [...prevIndexes, index] ); }, []); @@ -51,57 +84,85 @@ export default function CircuitsMenu({ ], }, { - header: "Custom Logic Elements", - gates: [], + header: "Custom Circuits", + gates: customBlocks.map(block => ({ + id: `custom-${block.id}`, // Префикс для идентификации кастомных блоков + label: block.name, + icon: (props) => ( + + ), + customData: block // Сохраняем полные данные блока для spawnCircuit + })), }, ]; + // Обработчик для создания кастомного блока + const handleSpawnCustomCircuit = useCallback((nodeId) => { + // Ищем полные данные блока по ID + const blockId = nodeId.replace('custom-', ''); + const block = customBlocks.find(b => b.id === blockId); + + if (block) { + // Вызываем функцию spawnCircuit с полными данными схемы + spawnCircuit(block.originalSchema); + } + }, [customBlocks, spawnCircuit]); + return ( -
-
-
-

Menu

-
-
+
+
+
+

Menu

+
+
-
    - {menuItems.map((item, index) => ( -
  1. -
    toggleItem(index)}> - {item.header} - -
    +
      + {menuItems.map((item, index) => ( +
    1. +
      toggleItem(index)}> + {item.header} + +
      -
      -
      - {item.gates.map((node) => ( -
      onDragStart(e, node.id)} - title={node.label} - > - +
      +
      + {item.gates.map((node) => ( +
      onDragStart(e, node.id)} + title={node.label} + > + +
      + ))}
      - ))} -
      -
      -
    2. - ))} -
    +
+ + ))} + +
-
); -} +} \ No newline at end of file diff --git a/UI/src/components/pages/mainPage/customCircuit.jsx b/UI/src/components/pages/mainPage/customCircuit.jsx new file mode 100644 index 00000000..761feb7c --- /dev/null +++ b/UI/src/components/pages/mainPage/customCircuit.jsx @@ -0,0 +1,213 @@ +import React, { useState } from 'react'; +import {IconCloseCross} from "../../../../assets/ui-icons.jsx"; +import toast from "react-hot-toast"; +import {showToastError} from "../../codeComponents/logger.jsx"; + + +export default function CreateCustomBlock({ + nodes, + edges, + onCreateFromFile, // Колбэк при создании из файла (вы реализуете) + onCreateFromCurrent // Колбэк при создании из схемы (вы реализуете) + }) { + const [isModalOpen, setIsModalOpen] = useState(false); + const [blockName, setBlockName] = useState(''); + const [error, setError] = useState(''); + + const handleCreateFromCurrent = () => { + if (!blockName.trim()) { + showToastError("Please enter a custom block name.") + return; + } + + try { + // Создаем кастомный блок из текущей схемы + const customBlock = createCustomBlock(nodes, edges, blockName.trim()); + + // Сохраняем в localStorage + saveCustomBlock(customBlock); + + // Сбрасываем состояние + setBlockName(''); + setError(''); + setIsModalOpen(false); + + // Вызываем колбэк (если нужна дополнительная логика) + if (onCreateFromCurrent) { + onCreateFromCurrent(customBlock); + } + + alert(`Блок "${blockName}" успешно создан!`); + } catch (err) { + console.error('Ошибка при создании блока:', err); + setError(`Ошибка: ${err.message}`); + } + }; + + const handleCreateFromFile = () => { + setIsModalOpen(false); + if (onCreateFromFile) { + onCreateFromFile(); + } + }; + + return ( +
+ + + + {isModalOpen && ( +
+
+

Create custom block

+ + + + + +
+ + +
+ + +
+ setBlockName(e.target.value)} + placeholder="New custom block name" + required + /> + {error &&

{error}

} +
+
+
+
+
+ )} +
+ ); +} + + + + + + + +const generateCustomBlockId = () => { + const timestamp = Date.now().toString(36); + const random = Math.random().toString(36).substring(2, 10); + return `custom_${timestamp}_${random}`; +}; + + +export const createCustomBlock = (nodes, edges, blockName) => { + // Валидация входных данных + if (!Array.isArray(nodes)) { + throw new Error('Invalid nodes: must be an array'); + } + + if (!Array.isArray(edges)) { + throw new Error('Invalid edges: must be an array'); + } + + // Фильтрация входных нод + const inputs = nodes.reduce((acc, node) => { + if (node.type === 'inputNodeSwitch' || node.type === 'inputNodeButton') { + acc.push({ + id: node.id, + name: node.name || `input_${Math.floor(Math.random() * 10000)}` // Случайный номер + }); + } + return acc; + }, []); + + // Фильтрация выходных нод + const outputs = nodes.reduce((acc, node) => { + if (node.type === 'outputNodeLed') { + acc.push({ + id: node.id, + name: node.name || `output_${Math.floor(Math.random() * 10000)}` // Случайный номер + }); + } + return acc; + }, []); + + return { + id: generateCustomBlockId(), + name: blockName, + inputs, + outputs, + originalSchema: { nodes, edges } // Сохраняем полную схему + }; +}; + +/** + * Сохраняет кастомный блок в localStorage + */ +export const saveCustomBlock = (customBlock) => { + try { + const savedBlocks = JSON.parse(localStorage.getItem('customBlocks') || '[]'); + const updatedBlocks = [...savedBlocks, customBlock]; + localStorage.setItem('customBlocks', JSON.stringify(updatedBlocks)); + } catch (error) { + console.error('Failed to save custom block:', error); + } +}; + +/** + * Загружает все кастомные блоки из localStorage + */ +export const loadCustomBlocks = () => { + try { + return JSON.parse(localStorage.getItem('customBlocks') || '[]'); + } catch (error) { + console.error('Failed to load custom blocks:', error); + return []; + } +}; + +/** + * Удаляет кастомный блок по ID + */ +export const deleteCustomBlock = (blockId) => { + try { + const savedBlocks = JSON.parse(localStorage.getItem('customBlocks') || '[]'); + const updatedBlocks = savedBlocks.filter(block => block.id !== blockId); + localStorage.setItem('customBlocks', JSON.stringify(updatedBlocks)); + return true; + } catch (error) { + console.error('Failed to delete custom block:', error); + return false; + } +}; + +/** + * Находит кастомный блок по ID + */ +export const findCustomBlockById = (blockId) => { + const blocks = loadCustomBlocks(); + return blocks.find(block => block.id === blockId); +}; \ No newline at end of file From 9230e54b3ff56c750a468d92df76c6d580387472 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sat, 19 Jul 2025 14:13:56 +0300 Subject: [PATCH 089/152] name editor above backdrop show --- UI/src/CSS/name-editor.css | 2 +- UI/src/components/pages/mainPage.jsx | 27 ++++++++++++++++----- UI/src/components/utils/handleNameChange.js | 4 +-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/UI/src/CSS/name-editor.css b/UI/src/CSS/name-editor.css index 5a299e33..27c0c3b2 100644 --- a/UI/src/CSS/name-editor.css +++ b/UI/src/CSS/name-editor.css @@ -8,7 +8,7 @@ border: 1px solid var(--main-5); border-radius: 0.5rem; user-select: none; - z-index: 10; + z-index: 2001; } .name-editor label { diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 02e756fd..7c92f499 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -69,13 +69,16 @@ import { handleNameChange } from "../utils/handleNameChange.js"; export const SimulateStateContext = createContext({ simulateState: "idle", - setSimulateState: () => {}, - updateInputState: () => {}, + setSimulateState: () => { + }, + updateInputState: () => { + }, }); export const NotificationsLevelContext = createContext({ logLevel: "idle", - setLogLevel: () => {}, + setLogLevel: () => { + }, }); export function useSimulateState() { @@ -142,8 +145,7 @@ export default function Main() { [nodes, edges], ); - const onNameChange = (e) => - handleNameChange(e, editableNode, setNodes, recordHistory); + const onNameChange = (e) => handleNameChange(e, editableNode, setNodes); const handleOpenClick = () => { if (fileInputRef.current) { @@ -649,11 +651,16 @@ export default function Main() { if (e.key === "Enter") { e.preventDefault(); deselectAll(); + setTimeout(recordHistory, 0); } }} autoFocus /> -
@@ -733,6 +740,14 @@ export default function Main() { onClick={() => closeMenu()} /> +
{ + deselectAll(); + setTimeout(recordHistory, 0); + }} + /> + Date: Sat, 19 Jul 2025 11:14:21 +0000 Subject: [PATCH 090/152] Automated formatting --- UI/src/components/pages/mainPage.jsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 7c92f499..3aeb0368 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -69,16 +69,13 @@ import { handleNameChange } from "../utils/handleNameChange.js"; export const SimulateStateContext = createContext({ simulateState: "idle", - setSimulateState: () => { - }, - updateInputState: () => { - }, + setSimulateState: () => {}, + updateInputState: () => {}, }); export const NotificationsLevelContext = createContext({ logLevel: "idle", - setLogLevel: () => { - }, + setLogLevel: () => {}, }); export function useSimulateState() { @@ -656,11 +653,14 @@ export default function Main() { }} autoFocus /> -
From 455e0fecaad7a56db250b51dc28d29e20ebb1d1b Mon Sep 17 00:00:00 2001 From: rii Date: Sat, 19 Jul 2025 14:22:50 +0300 Subject: [PATCH 091/152] custom blocks (very raw) --- UI/src/components/circuits/customBlock.jsx | 176 +++++++++++---------- UI/src/components/codeComponents/nodes.js | 2 + 2 files changed, 95 insertions(+), 83 deletions(-) diff --git a/UI/src/components/circuits/customBlock.jsx b/UI/src/components/circuits/customBlock.jsx index c0f795b1..0746a903 100644 --- a/UI/src/components/circuits/customBlock.jsx +++ b/UI/src/components/circuits/customBlock.jsx @@ -1,102 +1,112 @@ -import { useState, useRef, useEffect } from "react"; -import { Position } from "@xyflow/react"; -import CustomHandle from "../../codeComponents/CustomHandle.jsx"; -import { useSimulateState } from "../../pages/mainPage.jsx"; -import { useRotatedNode } from "../../hooks/useRotatedNode.jsx"; +import React, { useState } from "react"; +import { Position, useReactFlow } from "@xyflow/react"; +import CustomHandle from "../codeComponents/CustomHandle.jsx"; +import { useRotatedNode } from "../hooks/useRotatedNode.jsx"; -function InputNodeButton({ id, data, isConnectable }) { - const { simulateState, updateInputState } = useSimulateState(); - const [inputState, setInputState] = useState(data.value || false); - const cooldownRef = useRef(false); - const delay = 500; +function CustomCircuitNode({ id, data, isConnectable }) { + const { setNodes, setEdges } = useReactFlow(); + const [expanded, setExpanded] = useState(false); const rotation = data.rotation || 0; - const { getHandlePosition, RotatedNodeWrapper, triggerUpdate } = - useRotatedNode(id, rotation, 60, 80); + const { getHandlePosition, RotatedNodeWrapper } = + useRotatedNode(id, rotation, 120, 80); - const getHandleStyle = () => { - switch (rotation) { - case 90: - return { top: 32, left: 59 }; - case 180: - return { top: 38.5, left: 59 }; - case 270: - return { top: 39.5, left: 59 }; - default: - return { top: 40, left: 52 }; - } - }; + // Обработчик двойного клика для развертывания схемы + const handleDoubleClick = () => { + if (!data.originalSchema) return; - // Обновлять локальное состояние при изменении data.value - useEffect(() => { - setInputState(data.value || false); - }, [data.value]); + // Рассчитываем позицию для новых узлов + const position = { + x: data.position.x + 150, + y: data.position.y + }; - // 🧼 Дополнительный триггер для обновления DOM после нажатий - useEffect(() => { - if (typeof triggerUpdate === "function") { - triggerUpdate(); - } - }, [inputState, rotation]); + // Создаем узлы из схемы с новыми позициями + const newNodes = data.originalSchema.nodes.map(node => ({ + ...node, + position: { + x: node.position.x + position.x, + y: node.position.y + position.y + }, + selected: false, + zIndex: 1000 + })); - const handleChange = (newValue) => { - setInputState(newValue); - if (simulateState === "running" && updateInputState) { - updateInputState(id, newValue); - } - data.value = newValue; - }; + // Создаем соединения + const newEdges = [...data.originalSchema.edges]; - const handlePressDown = (e) => { - e.stopPropagation(); - if (cooldownRef.current || inputState) return; - handleChange(true); - }; + // Добавляем новые элементы в редактор + setNodes(prev => [...prev, ...newNodes]); + setEdges(prev => [...prev, ...newEdges]); - const handlePressUp = (e) => { - e.stopPropagation(); - e.preventDefault(); - if (!inputState) return; + // Удаляем кастомный узел + setNodes(prev => prev.filter(node => node.id !== id)); + }; - cooldownRef.current = true; - setTimeout(() => { - handleChange(false); - cooldownRef.current = false; - }, delay); + // Обработчик клика по узлу + const handleNodeClick = (e) => { + if (e.detail === 2) { + handleDoubleClick(); + } }; return ( - -

Button

+ +
+
+
{data.label || "Custom Circuit"}
+
- +
+ {/* Входные порты */} +
+ {data.inputs?.map((input, index) => ( +
+ + {input.name} +
+ ))} +
- + {/* Выходные порты */} +
+ {data.outputs?.map((output, index) => ( +
+ {output.name} + +
+ ))} +
+
+ + {expanded && ( +
+ {/* Здесь можно отобразить миниатюру схемы */} +
+ )}
); } -const SvgButton = ({ pressed, onPressDown, onPressUp, disabled }) => { - return ( -
-
-
- ); -}; - -export default InputNodeButton; +export default CustomCircuitNode; \ No newline at end of file diff --git a/UI/src/components/codeComponents/nodes.js b/UI/src/components/codeComponents/nodes.js index 1b7500cb..fcddb4dc 100644 --- a/UI/src/components/codeComponents/nodes.js +++ b/UI/src/components/codeComponents/nodes.js @@ -8,6 +8,7 @@ import InputNodeSwitch from "../circuits/IOelemnts/switch.jsx"; import InputNodeButton from "../circuits/IOelemnts/button.jsx"; import OutputNodeLed from "../circuits/IOelemnts/led.jsx"; import SwitchNode from "../circuits/IOelemnts/switch.jsx"; +import CustomBlock from "../circuits/customBlock.jsx"; export const nodeTypes = { andNode: AndNode, @@ -20,4 +21,5 @@ export const nodeTypes = { inputNodeButton: InputNodeButton, outputNodeLed: OutputNodeLed, switchNode: SwitchNode, + customBlock: CustomBlock, }; From 004e6548142d19957517073bf87e368ebe23f101 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:23:21 +0000 Subject: [PATCH 092/152] Automated formatting --- UI/src/CSS/customBlock.css | 151 ++++---- UI/src/components/circuits/customBlock.jsx | 186 +++++----- UI/src/components/pages/mainPage.jsx | 12 +- .../pages/mainPage/circuitsMenu.jsx | 170 ++++----- .../pages/mainPage/customCircuit.jsx | 329 +++++++++--------- 5 files changed, 423 insertions(+), 425 deletions(-) diff --git a/UI/src/CSS/customBlock.css b/UI/src/CSS/customBlock.css index 5a3b7188..b3c8d7c5 100644 --- a/UI/src/CSS/customBlock.css +++ b/UI/src/CSS/customBlock.css @@ -1,114 +1,113 @@ .create-button { - height: 2rem; - padding: 0 15px; - background-color: var(--status-success-1); - color: var(--main-0); - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 16px; - transition: background-color 0.3s; - position: fixed; - left: 5.64rem; - top: calc(0.5rem + 5vh); + height: 2rem; + padding: 0 15px; + background-color: var(--status-success-1); + color: var(--main-0); + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.3s; + position: fixed; + left: 5.64rem; + top: calc(0.5rem + 5vh); } - .custom-block-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; } .modal-content { - border: 1px solid var(--main-5); - background: var(--main-2); - padding: 25px; - color: var(--main-0); - border-radius: 8px; - width: 400px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - position: relative; + border: 1px solid var(--main-5); + background: var(--main-2); + padding: 25px; + color: var(--main-0); + border-radius: 8px; + width: 400px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + position: relative; - & > h3 { - font-size: 1.2rem; - margin-bottom: 40px; - } + & > h3 { + font-size: 1.2rem; + margin-bottom: 40px; + } } .close-button-custom-circuit { - position: absolute; - top: 10px; - right: 15px; - font-size: 24px; - border: none; - background: none; - cursor: pointer; + position: absolute; + top: 10px; + right: 15px; + font-size: 24px; + border: none; + background: none; + cursor: pointer; } .close-custom-circuit-cross { - width: 1rem; - height: 1rem; - color: var(--main-0); + width: 1rem; + height: 1rem; + color: var(--main-0); } .creation-options { - display: flex; - flex-direction: column; - gap: 10px; + display: flex; + flex-direction: column; + gap: 10px; } .option-button { - color: var(--main-0); - padding: 12px 20px; - background-color: var(--main-3); - border: 1px solid var(--main-4); - border-radius: 4px; - cursor: pointer; - font-size: 16px; - text-align: center; - transition: background-color 0.15s; + color: var(--main-0); + padding: 12px 20px; + background-color: var(--main-3); + border: 1px solid var(--main-4); + border-radius: 4px; + cursor: pointer; + font-size: 16px; + text-align: center; + transition: background-color 0.15s; } .option-button:hover { - background-color: var(--main-4); + background-color: var(--main-4); } .current-circuit-option { - display: flex; - flex-direction: column; - gap: 10px; + display: flex; + flex-direction: column; + gap: 10px; } .name-input { - margin-top: 10px; - display: flex; - flex-direction: column; + margin-top: 10px; + display: flex; + flex-direction: column; } .name-input input { - padding: 10px; - border: 1px solid var(--main-4); - background: var(--main-2); - border-radius: 4px; - font-size: 16px; - color: var(--main-0); + padding: 10px; + border: 1px solid var(--main-4); + background: var(--main-2); + border-radius: 4px; + font-size: 16px; + color: var(--main-0); } .name-input input:focus { - outline: none; - border-color: var(--select-1); - box-shadow: 0 0 0 2px var(--select-1); + outline: none; + border-color: var(--select-1); + box-shadow: 0 0 0 2px var(--select-1); } .error-message { - color: #f44336; - font-size: 14px; - margin-top: 5px; -} \ No newline at end of file + color: #f44336; + font-size: 14px; + margin-top: 5px; +} diff --git a/UI/src/components/circuits/customBlock.jsx b/UI/src/components/circuits/customBlock.jsx index 0746a903..5ea762d4 100644 --- a/UI/src/components/circuits/customBlock.jsx +++ b/UI/src/components/circuits/customBlock.jsx @@ -4,109 +4,113 @@ import CustomHandle from "../codeComponents/CustomHandle.jsx"; import { useRotatedNode } from "../hooks/useRotatedNode.jsx"; function CustomCircuitNode({ id, data, isConnectable }) { - const { setNodes, setEdges } = useReactFlow(); - const [expanded, setExpanded] = useState(false); - const rotation = data.rotation || 0; + const { setNodes, setEdges } = useReactFlow(); + const [expanded, setExpanded] = useState(false); + const rotation = data.rotation || 0; - const { getHandlePosition, RotatedNodeWrapper } = - useRotatedNode(id, rotation, 120, 80); + const { getHandlePosition, RotatedNodeWrapper } = useRotatedNode( + id, + rotation, + 120, + 80, + ); - // Обработчик двойного клика для развертывания схемы - const handleDoubleClick = () => { - if (!data.originalSchema) return; + // Обработчик двойного клика для развертывания схемы + const handleDoubleClick = () => { + if (!data.originalSchema) return; - // Рассчитываем позицию для новых узлов - const position = { - x: data.position.x + 150, - y: data.position.y - }; + // Рассчитываем позицию для новых узлов + const position = { + x: data.position.x + 150, + y: data.position.y, + }; - // Создаем узлы из схемы с новыми позициями - const newNodes = data.originalSchema.nodes.map(node => ({ - ...node, - position: { - x: node.position.x + position.x, - y: node.position.y + position.y - }, - selected: false, - zIndex: 1000 - })); + // Создаем узлы из схемы с новыми позициями + const newNodes = data.originalSchema.nodes.map((node) => ({ + ...node, + position: { + x: node.position.x + position.x, + y: node.position.y + position.y, + }, + selected: false, + zIndex: 1000, + })); - // Создаем соединения - const newEdges = [...data.originalSchema.edges]; + // Создаем соединения + const newEdges = [...data.originalSchema.edges]; - // Добавляем новые элементы в редактор - setNodes(prev => [...prev, ...newNodes]); - setEdges(prev => [...prev, ...newEdges]); + // Добавляем новые элементы в редактор + setNodes((prev) => [...prev, ...newNodes]); + setEdges((prev) => [...prev, ...newEdges]); - // Удаляем кастомный узел - setNodes(prev => prev.filter(node => node.id !== id)); - }; + // Удаляем кастомный узел + setNodes((prev) => prev.filter((node) => node.id !== id)); + }; - // Обработчик клика по узлу - const handleNodeClick = (e) => { - if (e.detail === 2) { - handleDoubleClick(); - } - }; + // Обработчик клика по узлу + const handleNodeClick = (e) => { + if (e.detail === 2) { + handleDoubleClick(); + } + }; - return ( - -
-
-
{data.label || "Custom Circuit"}
-
+ return ( + +
+
+
{data.label || "Custom Circuit"}
+
-
- {/* Входные порты */} -
- {data.inputs?.map((input, index) => ( -
- - {input.name} -
- ))} -
+
+ {/* Входные порты */} +
+ {data.inputs?.map((input, index) => ( +
+ + {input.name} +
+ ))} +
- {/* Выходные порты */} -
- {data.outputs?.map((output, index) => ( -
- {output.name} - -
- ))} -
+ {/* Выходные порты */} +
+ {data.outputs?.map((output, index) => ( +
+ {output.name} +
+ ))} +
+
- {expanded && ( -
- {/* Здесь можно отобразить миниатюру схемы */} -
- )} - - ); + {expanded && ( +
+ {/* Здесь можно отобразить миниатюру схемы */} +
+ )} + + ); } -export default CustomCircuitNode; \ No newline at end of file +export default CustomCircuitNode; diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 6525a8bd..e2c52d41 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -746,10 +746,14 @@ export default function Main() { /> {console.log("создать из файла")}} - onCreateFromCurrent={() => {console.log("создать из текущего")}} + nodes={nodes} + edges={edges} + onCreateFromFile={() => { + console.log("создать из файла"); + }} + onCreateFromCurrent={() => { + console.log("создать из текущего"); + }} /> { return ( -
-
- {inputs.map((input, index) => ( -
- ))} -
-
-
- {outputs.map((output, index) => ( -
- ))} -
+
+
+ {inputs.map((input, index) => ( +
+ ))} +
+
+
+ {outputs.map((output, index) => ( +
+ ))}
+
); }; export default function CircuitsMenu({ - circuitsMenuState, - onDragStart, - spawnCircuit, - }) { + circuitsMenuState, + onDragStart, + spawnCircuit, +}) { const [openIndexes, setOpenIndexes] = useState([]); const [customBlocks, setCustomBlocks] = useState([]); @@ -53,9 +53,9 @@ export default function CircuitsMenu({ const toggleItem = useCallback((index) => { setOpenIndexes((prevIndexes) => - prevIndexes.includes(index) - ? prevIndexes.filter((i) => i !== index) - : [...prevIndexes, index] + prevIndexes.includes(index) + ? prevIndexes.filter((i) => i !== index) + : [...prevIndexes, index], ); }, []); @@ -85,84 +85,88 @@ export default function CircuitsMenu({ }, { header: "Custom Circuits", - gates: customBlocks.map(block => ({ + gates: customBlocks.map((block) => ({ id: `custom-${block.id}`, // Префикс для идентификации кастомных блоков label: block.name, icon: (props) => ( - + ), - customData: block // Сохраняем полные данные блока для spawnCircuit + customData: block, // Сохраняем полные данные блока для spawnCircuit })), }, ]; // Обработчик для создания кастомного блока - const handleSpawnCustomCircuit = useCallback((nodeId) => { - // Ищем полные данные блока по ID - const blockId = nodeId.replace('custom-', ''); - const block = customBlocks.find(b => b.id === blockId); + const handleSpawnCustomCircuit = useCallback( + (nodeId) => { + // Ищем полные данные блока по ID + const blockId = nodeId.replace("custom-", ""); + const block = customBlocks.find((b) => b.id === blockId); - if (block) { - // Вызываем функцию spawnCircuit с полными данными схемы - spawnCircuit(block.originalSchema); - } - }, [customBlocks, spawnCircuit]); + if (block) { + // Вызываем функцию spawnCircuit с полными данными схемы + spawnCircuit(block.originalSchema); + } + }, + [customBlocks, spawnCircuit], + ); return ( -
-
-
-

Menu

-
-
+
+
+
+

Menu

+
+
-
    - {menuItems.map((item, index) => ( -
  1. -
    toggleItem(index)}> - {item.header} - -
    +
      + {menuItems.map((item, index) => ( +
    1. +
      toggleItem(index)}> + {item.header} + +
      -
      -
      - {item.gates.map((node) => ( -
      onDragStart(e, node.id)} - title={node.label} - > - -
      - ))} +
      +
      + {item.gates.map((node) => ( +
      onDragStart(e, node.id)} + title={node.label} + > +
      -
      -
    2. - ))} -
    -
+ ))} +
+
+ + ))} +
+
); -} \ No newline at end of file +} diff --git a/UI/src/components/pages/mainPage/customCircuit.jsx b/UI/src/components/pages/mainPage/customCircuit.jsx index 761feb7c..18943356 100644 --- a/UI/src/components/pages/mainPage/customCircuit.jsx +++ b/UI/src/components/pages/mainPage/customCircuit.jsx @@ -1,213 +1,200 @@ -import React, { useState } from 'react'; -import {IconCloseCross} from "../../../../assets/ui-icons.jsx"; +import React, { useState } from "react"; +import { IconCloseCross } from "../../../../assets/ui-icons.jsx"; import toast from "react-hot-toast"; -import {showToastError} from "../../codeComponents/logger.jsx"; - +import { showToastError } from "../../codeComponents/logger.jsx"; export default function CreateCustomBlock({ - nodes, - edges, - onCreateFromFile, // Колбэк при создании из файла (вы реализуете) - onCreateFromCurrent // Колбэк при создании из схемы (вы реализуете) - }) { - const [isModalOpen, setIsModalOpen] = useState(false); - const [blockName, setBlockName] = useState(''); - const [error, setError] = useState(''); - - const handleCreateFromCurrent = () => { - if (!blockName.trim()) { - showToastError("Please enter a custom block name.") - return; - } - - try { - // Создаем кастомный блок из текущей схемы - const customBlock = createCustomBlock(nodes, edges, blockName.trim()); - - // Сохраняем в localStorage - saveCustomBlock(customBlock); - - // Сбрасываем состояние - setBlockName(''); - setError(''); - setIsModalOpen(false); - - // Вызываем колбэк (если нужна дополнительная логика) - if (onCreateFromCurrent) { - onCreateFromCurrent(customBlock); - } - - alert(`Блок "${blockName}" успешно создан!`); - } catch (err) { - console.error('Ошибка при создании блока:', err); - setError(`Ошибка: ${err.message}`); - } - }; - - const handleCreateFromFile = () => { - setIsModalOpen(false); - if (onCreateFromFile) { - onCreateFromFile(); - } - }; - - return ( -
+ nodes, + edges, + onCreateFromFile, // Колбэк при создании из файла (вы реализуете) + onCreateFromCurrent, // Колбэк при создании из схемы (вы реализуете) +}) { + const [isModalOpen, setIsModalOpen] = useState(false); + const [blockName, setBlockName] = useState(""); + const [error, setError] = useState(""); + + const handleCreateFromCurrent = () => { + if (!blockName.trim()) { + showToastError("Please enter a custom block name."); + return; + } + + try { + // Создаем кастомный блок из текущей схемы + const customBlock = createCustomBlock(nodes, edges, blockName.trim()); + + // Сохраняем в localStorage + saveCustomBlock(customBlock); + + // Сбрасываем состояние + setBlockName(""); + setError(""); + setIsModalOpen(false); + + // Вызываем колбэк (если нужна дополнительная логика) + if (onCreateFromCurrent) { + onCreateFromCurrent(customBlock); + } + + alert(`Блок "${blockName}" успешно создан!`); + } catch (err) { + console.error("Ошибка при создании блока:", err); + setError(`Ошибка: ${err.message}`); + } + }; + + const handleCreateFromFile = () => { + setIsModalOpen(false); + if (onCreateFromFile) { + onCreateFromFile(); + } + }; + + return ( +
+ + + {isModalOpen && ( +
+
+

Create custom block

- {isModalOpen && ( -
-
-

Create custom block

- - - - - -
- - -
- - -
- setBlockName(e.target.value)} - placeholder="New custom block name" - required - /> - {error &&

{error}

} -
-
-
-
+
+ + +
+ + +
+ setBlockName(e.target.value)} + placeholder="New custom block name" + required + /> + {error &&

{error}

}
- )} +
+
+
- ); + )} +
+ ); } - - - - - - const generateCustomBlockId = () => { - const timestamp = Date.now().toString(36); - const random = Math.random().toString(36).substring(2, 10); - return `custom_${timestamp}_${random}`; + const timestamp = Date.now().toString(36); + const random = Math.random().toString(36).substring(2, 10); + return `custom_${timestamp}_${random}`; }; - export const createCustomBlock = (nodes, edges, blockName) => { - // Валидация входных данных - if (!Array.isArray(nodes)) { - throw new Error('Invalid nodes: must be an array'); + // Валидация входных данных + if (!Array.isArray(nodes)) { + throw new Error("Invalid nodes: must be an array"); + } + + if (!Array.isArray(edges)) { + throw new Error("Invalid edges: must be an array"); + } + + // Фильтрация входных нод + const inputs = nodes.reduce((acc, node) => { + if (node.type === "inputNodeSwitch" || node.type === "inputNodeButton") { + acc.push({ + id: node.id, + name: node.name || `input_${Math.floor(Math.random() * 10000)}`, // Случайный номер + }); } - - if (!Array.isArray(edges)) { - throw new Error('Invalid edges: must be an array'); + return acc; + }, []); + + // Фильтрация выходных нод + const outputs = nodes.reduce((acc, node) => { + if (node.type === "outputNodeLed") { + acc.push({ + id: node.id, + name: node.name || `output_${Math.floor(Math.random() * 10000)}`, // Случайный номер + }); } - - // Фильтрация входных нод - const inputs = nodes.reduce((acc, node) => { - if (node.type === 'inputNodeSwitch' || node.type === 'inputNodeButton') { - acc.push({ - id: node.id, - name: node.name || `input_${Math.floor(Math.random() * 10000)}` // Случайный номер - }); - } - return acc; - }, []); - - // Фильтрация выходных нод - const outputs = nodes.reduce((acc, node) => { - if (node.type === 'outputNodeLed') { - acc.push({ - id: node.id, - name: node.name || `output_${Math.floor(Math.random() * 10000)}` // Случайный номер - }); - } - return acc; - }, []); - - return { - id: generateCustomBlockId(), - name: blockName, - inputs, - outputs, - originalSchema: { nodes, edges } // Сохраняем полную схему - }; + return acc; + }, []); + + return { + id: generateCustomBlockId(), + name: blockName, + inputs, + outputs, + originalSchema: { nodes, edges }, // Сохраняем полную схему + }; }; /** * Сохраняет кастомный блок в localStorage */ export const saveCustomBlock = (customBlock) => { - try { - const savedBlocks = JSON.parse(localStorage.getItem('customBlocks') || '[]'); - const updatedBlocks = [...savedBlocks, customBlock]; - localStorage.setItem('customBlocks', JSON.stringify(updatedBlocks)); - } catch (error) { - console.error('Failed to save custom block:', error); - } + try { + const savedBlocks = JSON.parse( + localStorage.getItem("customBlocks") || "[]", + ); + const updatedBlocks = [...savedBlocks, customBlock]; + localStorage.setItem("customBlocks", JSON.stringify(updatedBlocks)); + } catch (error) { + console.error("Failed to save custom block:", error); + } }; /** * Загружает все кастомные блоки из localStorage */ export const loadCustomBlocks = () => { - try { - return JSON.parse(localStorage.getItem('customBlocks') || '[]'); - } catch (error) { - console.error('Failed to load custom blocks:', error); - return []; - } + try { + return JSON.parse(localStorage.getItem("customBlocks") || "[]"); + } catch (error) { + console.error("Failed to load custom blocks:", error); + return []; + } }; /** * Удаляет кастомный блок по ID */ export const deleteCustomBlock = (blockId) => { - try { - const savedBlocks = JSON.parse(localStorage.getItem('customBlocks') || '[]'); - const updatedBlocks = savedBlocks.filter(block => block.id !== blockId); - localStorage.setItem('customBlocks', JSON.stringify(updatedBlocks)); - return true; - } catch (error) { - console.error('Failed to delete custom block:', error); - return false; - } + try { + const savedBlocks = JSON.parse( + localStorage.getItem("customBlocks") || "[]", + ); + const updatedBlocks = savedBlocks.filter((block) => block.id !== blockId); + localStorage.setItem("customBlocks", JSON.stringify(updatedBlocks)); + return true; + } catch (error) { + console.error("Failed to delete custom block:", error); + return false; + } }; /** * Находит кастомный блок по ID */ export const findCustomBlockById = (blockId) => { - const blocks = loadCustomBlocks(); - return blocks.find(block => block.id === blockId); -}; \ No newline at end of file + const blocks = loadCustomBlocks(); + return blocks.find((block) => block.id === blockId); +}; From d7ecaa10e1e2f2fe468815e69bf873496949a2b6 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sat, 19 Jul 2025 14:45:13 +0300 Subject: [PATCH 093/152] tooltip added --- UI/src/CSS/name-editor.css | 70 ++++++++++++++++++++++++++++ UI/src/components/pages/mainPage.jsx | 53 ++++++++++++--------- 2 files changed, 102 insertions(+), 21 deletions(-) diff --git a/UI/src/CSS/name-editor.css b/UI/src/CSS/name-editor.css index 27c0c3b2..400af2a4 100644 --- a/UI/src/CSS/name-editor.css +++ b/UI/src/CSS/name-editor.css @@ -47,3 +47,73 @@ box-shadow: none; border: var(--select-1) solid 0.05rem; } + +.label-container { + display: flex; + margin-bottom: 6px; +} + +.tooltip-container { + position: relative; + margin-left: 45px; + cursor: help; +} + +.tooltip-icon { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 50%; + background-color: var(--main-0); + color: var(--menu-lighter); + font-size: 12px; + font-weight: bold; + transition: background-color 0.2s; +} + +.tooltip-icon:hover { + background-color: var(--select-1); +} + +.tooltip-text { + visibility: hidden; + width: 200px; + background-color: var(--menu-lighter); + color: var(--main-0); + text-align: center; + border-radius: 4px; + padding: 8px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + transform: translateX(-50%); + opacity: 0; + transition: opacity 0.3s; + font-size: 12px; + border: 1px solid var(--main-5); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + pointer-events: none; +} + +.tooltip-text::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: var(--menu-lighter) transparent transparent transparent; +} + +.tooltip-container:hover .tooltip-text { + visibility: visible; + opacity: 1; +} + +.input-group { + display: flex; +} \ No newline at end of file diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 3aeb0368..b0bbea49 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -639,30 +639,41 @@ export default function Main() { {editableNode && (
- - { - if (e.key === "Enter") { +
+ +
+
?
+
+ When creating custom circuit, each IO with an export + name will become one of the new circuit's outputs. +
+
+
+
+ { + if (e.key === "Enter") { + e.preventDefault(); + deselectAll(); + setTimeout(recordHistory, 0); + } + }} + autoFocus + /> + + }} + > + Close + +
)} From 10de068f35d3c14b1f266b0afeeb85e62335e28b Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sat, 19 Jul 2025 14:50:26 +0300 Subject: [PATCH 094/152] nodes spawn fixed --- UI/src/components/pages/mainPage.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index e2c52d41..106a06a7 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -340,7 +340,6 @@ export default function Main() { const { nodes: newNodes, edges: newEdges } = deselectAllUtil(nodes, edges); setNodes(newNodes); setEdges(newEdges); - recordHistory(); }, [nodes, edges, setNodes, setEdges]); const deleteSelectedElements = useCallback(() => { From bbde0929e8f095489d66321ac80eaed923e79e26 Mon Sep 17 00:00:00 2001 From: doshq Date: Sat, 19 Jul 2025 15:02:17 +0300 Subject: [PATCH 095/152] Add all remain functionality, tested successfully by unit testing --- backend/profile/database/db.py | 6 +-- .../{postgres_users.py => pgre_users.py} | 1 - backend/profile/routers/profile.py | 52 +++++++++++++++---- backend/profile/routers/project.py | 48 +++++++++++++++-- backend/profile/schemas.py | 6 ++- backend/profile/utils.py | 18 ++++--- backend/tests/profile_tests/conftest.py | 43 +++++++++++++++ .../profile_tests/test_create_project.py | 6 ++- .../test_get_forbidden_project.py | 36 +++++++++++++ .../tests/profile_tests/test_get_projects.py | 1 + .../profile_tests/test_update_project.py | 29 +++++++++++ 11 files changed, 217 insertions(+), 29 deletions(-) rename backend/profile/database/{postgres_users.py => pgre_users.py} (97%) create mode 100644 backend/tests/profile_tests/test_get_forbidden_project.py create mode 100644 backend/tests/profile_tests/test_update_project.py diff --git a/backend/profile/database/db.py b/backend/profile/database/db.py index 9f43e5f4..5fbfb176 100644 --- a/backend/profile/database/db.py +++ b/backend/profile/database/db.py @@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio.session import AsyncSession from backend.profile.database.pgre_projects import PostgreSQLProjectDatabase -from backend.profile.database.postgres_users import PostgreSQLUserDatabase +from backend.profile.database.pgre_users import PostgreSQLUserDatabase from backend.profile.models import User, ProjectModel from backend.profile.schemas import UserDB, UserProfile, ProjectDB @@ -36,8 +36,8 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, Any]: async def get_user_db(session: AsyncSession = Depends(get_async_session)): yield PostgreSQLUserDatabase( session=session, - user_model=User, # ваша SQLAlchemy модель - user_db_model=UserDB # ваша Pydantic схема + user_model=User, + user_db_model=UserDB ) async def get_project_db(session: AsyncSession = Depends(get_async_session)): diff --git a/backend/profile/database/postgres_users.py b/backend/profile/database/pgre_users.py similarity index 97% rename from backend/profile/database/postgres_users.py rename to backend/profile/database/pgre_users.py index 63666c5e..b750a334 100644 --- a/backend/profile/database/postgres_users.py +++ b/backend/profile/database/pgre_users.py @@ -75,7 +75,6 @@ async def delete(self, user: UP) -> None: await self.session.commit() def _convert_to_userdb(self, user: UserModel) -> UP: - """Конвертирует SQLAlchemy модель в Pydantic UserDB схему""" user_dict = { "id": user.id, "username": user.username, diff --git a/backend/profile/routers/profile.py b/backend/profile/routers/profile.py index 36d57acf..ce61e31c 100644 --- a/backend/profile/routers/profile.py +++ b/backend/profile/routers/profile.py @@ -1,13 +1,17 @@ from uuid import UUID from fastapi import APIRouter, HTTPException, Depends from backend.profile.database.db import get_user_db -from backend.profile.database.postgres_users import PostgreSQLUserDatabase -from backend.profile.schemas import UpdateName, UpdateEmail, UpdatePassword +from backend.profile.database.pgre_users import PostgreSQLUserDatabase +from backend.profile.schemas import UpdateName, UpdateEmail, UpdatePassword, UserDB +from backend.profile.utils import get_current_user router = APIRouter(prefix="/api/profile", tags=["profile"]) @router.get("/{id}") -async def get_profile(id: int, user_db: PostgreSQLUserDatabase = Depends(get_user_db)): +async def get_profile( + id: int, + current_user: UserDB = Depends(get_current_user), + user_db: PostgreSQLUserDatabase = Depends(get_user_db)): try: user = await user_db.get(id) except Exception as e: @@ -15,6 +19,12 @@ async def get_profile(id: int, user_db: PostgreSQLUserDatabase = Depends(get_use raise HTTPException(status_code=400, detail=str(e)) if not user: raise HTTPException(status_code=404, detail="User not found") + if id != current_user['id']: + return { + "id": user.id, + "username": user.username, + "name": user.name, + } return { "id": user.id, "username": user.username, @@ -23,9 +33,16 @@ async def get_profile(id: int, user_db: PostgreSQLUserDatabase = Depends(get_use } @router.patch("/{id}/name") -async def update_name(id: int, body: UpdateName, user_db: PostgreSQLUserDatabase = Depends(get_user_db)): # user_id: str = Depends(get_current_user_id)): - # if id != user_id: - # raise HTTPException(status_code=403, detail="You can only modify your own profile") +async def update_name( + id: int, + body: UpdateName, + current_user: UserDB = Depends(get_current_user), + user_db: PostgreSQLUserDatabase = Depends(get_user_db)): + if id != current_user['id']: + raise HTTPException( + status_code=403, + detail="You are not eligible to update other users' name" + ) user = await user_db.get(id) if not user: raise HTTPException(status_code=404, detail="User not found") @@ -33,13 +50,22 @@ async def update_name(id: int, body: UpdateName, user_db: PostgreSQLUserDatabase update_data = {"name": body.name} await user_db.update(user, update_dict=update_data) return {"status": "name updated"} -# + @router.patch("/{id}/email") -async def update_email(id: int, body: UpdateEmail, user_db: PostgreSQLUserDatabase = Depends(get_user_db)): +async def update_email( + id: int, + body: UpdateEmail, + current_user: UserDB = Depends(get_current_user), + user_db: PostgreSQLUserDatabase = Depends(get_user_db)): user = await user_db.get(id) if not user: raise HTTPException(status_code=404, detail="User not found") + if id != current_user['id']: + raise HTTPException( + status_code=403, + detail="You are not eligible to update other users' email" + ) update_data = {"email": body.email} await user_db.update(user, update_dict=update_data) return {"status": "email updated"} @@ -51,10 +77,18 @@ async def update_password(id: int, body: UpdatePassword, user_db: PostgreSQLUser raise HTTPException(status_code=400, detail="Password not supported") # @router.delete("/{id}") -async def delete_profile(id: int, user_db: PostgreSQLUserDatabase = Depends(get_user_db)): +async def delete_profile( + id: int, + current_user: UserDB = Depends(get_current_user), + user_db: PostgreSQLUserDatabase = Depends(get_user_db)): user = await user_db.get(id) if not user: raise HTTPException(status_code=404, detail="User not found") + if id != current_user['id']: + raise HTTPException( + status_code=403, + detail="You are not eligible to delete other users' profile" + ) await user_db.delete(user) return {"status": "deleted"} \ No newline at end of file diff --git a/backend/profile/routers/project.py b/backend/profile/routers/project.py index f43a526d..31b89747 100644 --- a/backend/profile/routers/project.py +++ b/backend/profile/routers/project.py @@ -1,26 +1,34 @@ from fastapi import APIRouter, Depends, HTTPException from backend.profile.database.db import get_project_db from backend.profile.database.pgre_projects import PostgreSQLProjectDatabase -from backend.profile.schemas import Project, ProjectDB +from backend.profile.schemas import Project, ProjectDB, UserDB, ProjectCreateResponse +from backend.profile.utils import get_current_user router = APIRouter(prefix="/api/profile/{id}/project", tags=["projects"]) -@router.post("", response_model=dict) +@router.post("", response_model=ProjectCreateResponse) async def create_project( id: int, project: Project, + current_user: UserDB = Depends(get_current_user), project_db: PostgreSQLProjectDatabase = Depends(get_project_db) ): + if current_user['id'] != id: + raise HTTPException(status_code=403, detail="Cannot create project for another user") project_dict = project.model_dump() project_dict["owner_id"] = id - await project_db.create(project_dict) - return {"status": "project created"} + created_project = await project_db.create(project_dict) + return { + "status": "project created", + "project_id": created_project.pid + } @router.get("", response_model=list[ProjectDB]) async def get_all_projects( id: int, + current_user: UserDB = Depends(get_current_user), project_db: PostgreSQLProjectDatabase = Depends(get_project_db) ): return await project_db.get_all(id) @@ -30,9 +38,15 @@ async def get_all_projects( async def get_project( id: int, pid: int, + current_user: UserDB = Depends(get_current_user), project_db: PostgreSQLProjectDatabase = Depends(get_project_db) ): - project = await project_db.get(id, pid) + if id != current_user['id']: + raise HTTPException( + status_code=403, + detail="You are not authorized to access this project" + ) + project = await project_db.get(current_user['id'], pid) if not project: raise HTTPException(status_code=404, detail="Project not found") return project @@ -43,8 +57,14 @@ async def update_project_name( id: int, pid: int, data: dict, + current_user: UserDB = Depends(get_current_user), project_db: PostgreSQLProjectDatabase = Depends(get_project_db) ): + if id != current_user['id']: + raise HTTPException( + status_code=403, + detail="You are not authorized to access this project" + ) if "name" not in data: raise HTTPException(status_code=400, detail="Name is required") @@ -57,8 +77,14 @@ async def update_project_circuit( id: int, pid: int, data: dict, + current_user: UserDB = Depends(get_current_user), project_db: PostgreSQLProjectDatabase = Depends(get_project_db) ): + if id != current_user['id']: + raise HTTPException( + status_code=403, + detail="You are not authorized to access this project" + ) if "circuit" not in data: raise HTTPException(status_code=400, detail="Circuit data is required") @@ -71,8 +97,14 @@ async def update_custom_nodes( id: int, pid: int, data: dict, + current_user: UserDB = Depends(get_current_user), project_db: PostgreSQLProjectDatabase = Depends(get_project_db) ): + if id != current_user['id']: + raise HTTPException( + status_code=403, + detail="You are not authorized to access this project" + ) if "custom_nodes" not in data: raise HTTPException(status_code=400, detail="Custom nodes data is required") @@ -84,7 +116,13 @@ async def update_custom_nodes( async def delete_project( id: int, pid: int, + current_user: UserDB = Depends(get_current_user), project_db: PostgreSQLProjectDatabase = Depends(get_project_db) ): + if id != current_user['id']: + raise HTTPException( + status_code=403, + detail="You are not authorized to access this project" + ) await project_db.delete(id, pid) return {"status": "project deleted"} \ No newline at end of file diff --git a/backend/profile/schemas.py b/backend/profile/schemas.py index fe3978c7..d26a76d7 100644 --- a/backend/profile/schemas.py +++ b/backend/profile/schemas.py @@ -53,9 +53,13 @@ class Project(schemas.BaseModel): verilog: Optional[str] = None custom_nodes: Optional[Dict] = None +class ProjectCreateResponse(schemas.BaseModel): + status: str + project_id: int + class ProjectDB(Project): pid: int owner_id: int class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/profile/utils.py b/backend/profile/utils.py index 86bc95a0..c2922a83 100644 --- a/backend/profile/utils.py +++ b/backend/profile/utils.py @@ -1,22 +1,24 @@ -import base64 import json - -from fastapi import Depends, HTTPException, Header import httpx +import base64 +from typing import Annotated +from fastapi import Depends, HTTPException, Header +from fastapi.security import OAuth2PasswordBearer + -async def get_current_user_id(authorization: str = Header(...)) -> str: +async def get_current_user(authorization: Annotated[str, Header(...)]) -> str: if not authorization.startswith("Bearer "): - raise HTTPException(status_code=401, detail="Missing or invalid Authorization header") + raise HTTPException(status_code=401, detail="Missing Bearer token") token = authorization.removeprefix("Bearer ").strip() - async with httpx.AsyncClient() as client: - response = await client.get("http://auth-backend:8000/auth/verify", headers={"Authorization": f"Bearer {token}"}) + async with httpx.AsyncClient(base_url="http://localhost:8080") as client: + response = await client.post("/api/auth/verify", headers={"Authorization": f"Bearer {token}"}) if response.status_code != 200: raise HTTPException(status_code=401, detail="Invalid or expired token") - return response.json().get("user_id") + return decode_jwt(token) def decode_jwt(token: str): try: diff --git a/backend/tests/profile_tests/conftest.py b/backend/tests/profile_tests/conftest.py index 2d0fae9a..cebab8c5 100644 --- a/backend/tests/profile_tests/conftest.py +++ b/backend/tests/profile_tests/conftest.py @@ -79,4 +79,47 @@ async def test_project_data(): ] }, # "custom_nodes": [] + } + +@pytest_asyncio.fixture +async def test_custom_node_data(): + return { + "name": "Test Project", + "id": "1", + "circuit": { + "nodes": [ + { + "id": "andNode_1751219192609", + "type": "andNode", + "position": { + "x": 130, + "y": 220 + }, + "data": { + "customId": "andNode_1751219192609" + } + }, + { + "id": "inputNodeSwitch_1751219193704", + "type": "inputNodeSwitch", + "position": { + "x": 10, + "y": 370 + }, + "data": { + "customId": "inputNodeSwitch_1751219193704" + } + } + ], + "edges": [ + { + "id": "xy-edge__inputNodeSwitch_1751219193704output-1-andNode_1751219192609input-1", + "source": "inputNodeSwitch_1751219193704", + "target": "andNode_1751219192609", + "sourceHandle": "output-1", + "targetHandle": "input-1" + } + ] + }, + # "custom_nodes": [] } \ No newline at end of file diff --git a/backend/tests/profile_tests/test_create_project.py b/backend/tests/profile_tests/test_create_project.py index 11c2006c..1cc14360 100644 --- a/backend/tests/profile_tests/test_create_project.py +++ b/backend/tests/profile_tests/test_create_project.py @@ -11,8 +11,10 @@ async def test_create_project(auth_client, registered_user, profile_client, test response = await profile_client.post( f"/api/profile/{id}/project", - json=test_project_data + json=test_project_data, + headers={"Authorization": f"Bearer {token}"} ) + print(response.json()["project_id"]) assert response.status_code == status.HTTP_200_OK - assert response.json() == {"status": "project created"} + assert response.json()["status"] == "project created" diff --git a/backend/tests/profile_tests/test_get_forbidden_project.py b/backend/tests/profile_tests/test_get_forbidden_project.py new file mode 100644 index 00000000..f77346a3 --- /dev/null +++ b/backend/tests/profile_tests/test_get_forbidden_project.py @@ -0,0 +1,36 @@ +import pytest +from fastapi import status +from backend.profile.utils import decode_jwt + + +@pytest.mark.asyncio +async def test_create_project(auth_client, registered_user, profile_client, test_project_data): + original_user, original_token = registered_user + user_data = { + "name": "AnotherUser", + "username": "wronguser", + "email": "test@example.ru", + "password": "TestPassword123" + } + response = await auth_client.post("/api/auth/register", json=user_data) + assert response.status_code == 201 or response.status_code == 409 + + login_response = await auth_client.post( + "/api/auth/login", + json={ + "login": user_data["username"], + "password": user_data["password"] + } + ) + token = login_response.json().get("access") + + id = decode_jwt(original_token)['id'] + pid = 1 + + response = await profile_client.get( + f"/api/profile/{id}/project/{pid}", + headers={"Authorization": f"Bearer {token}"} + ) + + print(response.json()) + assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/backend/tests/profile_tests/test_get_projects.py b/backend/tests/profile_tests/test_get_projects.py index a1f8a177..e87b3915 100644 --- a/backend/tests/profile_tests/test_get_projects.py +++ b/backend/tests/profile_tests/test_get_projects.py @@ -11,6 +11,7 @@ async def test_create_project(auth_client, registered_user, profile_client, test response = await profile_client.get( f"/api/profile/{id}/project", + headers={"Authorization": f"Bearer {token}"} ) print("GET: ", response.json()) diff --git a/backend/tests/profile_tests/test_update_project.py b/backend/tests/profile_tests/test_update_project.py new file mode 100644 index 00000000..d25a3e2d --- /dev/null +++ b/backend/tests/profile_tests/test_update_project.py @@ -0,0 +1,29 @@ +import pytest +from fastapi import status + +from backend.profile.utils import decode_jwt + + +@pytest.mark.asyncio +async def test_update_email(auth_client, registered_user, profile_client, test_custom_node_data): + user_data, token = registered_user + + id = decode_jwt(token)['id'] + + pid = 1 + update_response = await profile_client.patch( + f"/api/profile/{id}/project/{pid}/custom_nodes", + json={"custom_nodes": test_custom_node_data}, + headers={"Authorization": f"Bearer {token}"} + ) + + assert update_response.status_code == status.HTTP_200_OK + assert update_response.json()["status"] == "custom_nodes updated" + + profile_response = await profile_client.get( + f"/api/profile/{id}/project/{pid}", + headers={"Authorization": f"Bearer {token}"} + ) + + assert profile_response.status_code == status.HTTP_200_OK + assert profile_response.json()["custom_nodes"] \ No newline at end of file From 5db09dd33b0efc7b6f27c3d4dda36471f3875b35 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sat, 19 Jul 2025 15:32:29 +0300 Subject: [PATCH 096/152] handlers get bigger when hovered, projection edge color fixed, led input handler fixed --- UI/src/CSS/xy-theme.css | 33 ++++++++++---------- UI/src/components/circuits/IOelemnts/led.jsx | 1 + 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/UI/src/CSS/xy-theme.css b/UI/src/CSS/xy-theme.css index 915bf5d6..cac2180c 100644 --- a/UI/src/CSS/xy-theme.css +++ b/UI/src/CSS/xy-theme.css @@ -11,22 +11,6 @@ z-index: 5; } -/*.react-flow__edge-path {*/ -/* stroke-width: 2;*/ -/* stroke: var(--main-0);*/ -/*}*/ - -/* Hovered edge */ -/*.react-flow__edge.selectable:hover .react-flow__edge-path {*/ -/* stroke: var(--main-4);*/ -/*}*/ - -/*!* Selected edge *!*/ -/*.react-flow__edge.selectable.selected .react-flow__edge-path {*/ -/* !*stroke-width: 3;*!*/ -/* stroke: var(--main-4);*/ -/*}*/ - .react-flow__edge.selected .react-flow__edge-path { stroke: var(--main-4) !important; stroke-width: 2; @@ -52,14 +36,29 @@ background: var(--main-0); border: 0.05rem solid var(--main-5); object-fit: cover; - transition: filter 0.2s; z-index: 15; + + transform-origin: center center; + transition: transform 0.2s ease-in-out, background 0.2s ease-in-out; } .react-flow__handle:hover { + transform: scale(1.5); background: var(--main-4); } +.react-flow__handle[data-handlepos="top"] { transform-origin: 150% 150%; } +.react-flow__handle[data-handlepos="bottom"] { transform-origin: 150% -50%; } +.react-flow__handle[data-handlepos="left"] { transform-origin: 150% 150%; } +.react-flow__handle[data-handlepos="right"] { transform-origin: -50% 150%; } + +.react-flow { + /* Новый цвет проекционной линии */ + --xy-connectionline-stroke-default: var(--main-0); + /* (необязательно) ширина линии */ + --xy-connectionline-stroke-width-default: 2; +} + .react-flow__node { border: 0.05rem solid var(--main-5); border-radius: 4px; diff --git a/UI/src/components/circuits/IOelemnts/led.jsx b/UI/src/components/circuits/IOelemnts/led.jsx index c6f469b6..109d326c 100644 --- a/UI/src/components/circuits/IOelemnts/led.jsx +++ b/UI/src/components/circuits/IOelemnts/led.jsx @@ -64,6 +64,7 @@ function OutputNodeLed({ id, data, isConnectable }) { id="input-1" style={getHandleStyle()} isConnectable={isConnectable} + connections={1} />
); From 530d0da6c5e8060c3deab2825fd564bacfd89728 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sat, 19 Jul 2025 15:54:14 +0300 Subject: [PATCH 097/152] color of the hovered handlers changed --- UI/src/CSS/xy-theme.css | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/UI/src/CSS/xy-theme.css b/UI/src/CSS/xy-theme.css index cac2180c..5a2ef859 100644 --- a/UI/src/CSS/xy-theme.css +++ b/UI/src/CSS/xy-theme.css @@ -44,7 +44,7 @@ .react-flow__handle:hover { transform: scale(1.5); - background: var(--main-4); + background: var(--main-0); } .react-flow__handle[data-handlepos="top"] { transform-origin: 150% 150%; } @@ -53,9 +53,7 @@ .react-flow__handle[data-handlepos="right"] { transform-origin: -50% 150%; } .react-flow { - /* Новый цвет проекционной линии */ --xy-connectionline-stroke-default: var(--main-0); - /* (необязательно) ширина линии */ --xy-connectionline-stroke-width-default: 2; } From e549964cd0ec2ca69af7c271e1edf5fe686fa1e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 12:54:35 +0000 Subject: [PATCH 098/152] Automated formatting --- UI/src/CSS/name-editor.css | 2 +- UI/src/CSS/xy-theme.css | 20 +++++++++++++++----- UI/src/components/pages/mainPage.jsx | 4 ++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/UI/src/CSS/name-editor.css b/UI/src/CSS/name-editor.css index 400af2a4..ddcf1c1b 100644 --- a/UI/src/CSS/name-editor.css +++ b/UI/src/CSS/name-editor.css @@ -116,4 +116,4 @@ .input-group { display: flex; -} \ No newline at end of file +} diff --git a/UI/src/CSS/xy-theme.css b/UI/src/CSS/xy-theme.css index 5a2ef859..7c38f369 100644 --- a/UI/src/CSS/xy-theme.css +++ b/UI/src/CSS/xy-theme.css @@ -39,7 +39,9 @@ z-index: 15; transform-origin: center center; - transition: transform 0.2s ease-in-out, background 0.2s ease-in-out; + transition: + transform 0.2s ease-in-out, + background 0.2s ease-in-out; } .react-flow__handle:hover { @@ -47,10 +49,18 @@ background: var(--main-0); } -.react-flow__handle[data-handlepos="top"] { transform-origin: 150% 150%; } -.react-flow__handle[data-handlepos="bottom"] { transform-origin: 150% -50%; } -.react-flow__handle[data-handlepos="left"] { transform-origin: 150% 150%; } -.react-flow__handle[data-handlepos="right"] { transform-origin: -50% 150%; } +.react-flow__handle[data-handlepos="top"] { + transform-origin: 150% 150%; +} +.react-flow__handle[data-handlepos="bottom"] { + transform-origin: 150% -50%; +} +.react-flow__handle[data-handlepos="left"] { + transform-origin: 150% 150%; +} +.react-flow__handle[data-handlepos="right"] { + transform-origin: -50% 150%; +} .react-flow { --xy-connectionline-stroke-default: var(--main-0); diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index b0bbea49..9e0ae4d3 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -644,8 +644,8 @@ export default function Main() {
?
- When creating custom circuit, each IO with an export - name will become one of the new circuit's outputs. + When creating custom circuit, each IO with an export name + will become one of the new circuit's outputs.
From e34a543633087d2ab5b6763806fea0059a1c47ec Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sat, 19 Jul 2025 16:01:03 +0300 Subject: [PATCH 099/152] unit test fixed --- .../unit tests/handleNameChange.unit.test.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/UI/src/components/utils/__tests__/unit tests/handleNameChange.unit.test.js b/UI/src/components/utils/__tests__/unit tests/handleNameChange.unit.test.js index 35a53d3c..3c17a901 100644 --- a/UI/src/components/utils/__tests__/unit tests/handleNameChange.unit.test.js +++ b/UI/src/components/utils/__tests__/unit tests/handleNameChange.unit.test.js @@ -1,10 +1,7 @@ import { handleNameChange } from "../../handleNameChange"; -jest.useFakeTimers(); - describe("handleNameChange", () => { const mockSetNodes = jest.fn(); - const mockRecordHistory = jest.fn(); const editableNode = { id: "1", name: "Old Name" }; @@ -15,13 +12,12 @@ describe("handleNameChange", () => { beforeEach(() => { mockSetNodes.mockClear(); - mockRecordHistory.mockClear(); }); it("updates the name of the editable node", () => { const event = { target: { value: "New Name" } }; - handleNameChange(event, editableNode, mockSetNodes, mockRecordHistory); + handleNameChange(event, editableNode, mockSetNodes); expect(mockSetNodes).toHaveBeenCalledWith(expect.any(Function)); const updateFn = mockSetNodes.mock.calls[0][0]; @@ -31,17 +27,13 @@ describe("handleNameChange", () => { { id: "1", name: "New Name", type: "inputNodeSwitch" }, { id: "2", name: "Another Node", type: "inputNodeButton" }, ]); - - jest.runAllTimers(); - expect(mockRecordHistory).toHaveBeenCalled(); }); it("does nothing if editableNode is null", () => { const event = { target: { value: "Should Not Update" } }; - handleNameChange(event, null, mockSetNodes, mockRecordHistory); + handleNameChange(event, null, mockSetNodes); expect(mockSetNodes).not.toHaveBeenCalled(); - expect(mockRecordHistory).not.toHaveBeenCalled(); }); }); From 98ec484f2753af0df968272406bd5a009dfcf0ec Mon Sep 17 00:00:00 2001 From: doshq Date: Sat, 19 Jul 2025 17:25:29 +0300 Subject: [PATCH 100/152] Create Dockerfile, requirements.txt. Add in docker-compose.yml. Fix auth/verify connection. --- backend/Dockerfile | 13 +++++++++++++ backend/__init__.py | 0 backend/profile/main.py | 1 - backend/profile/utils.py | 4 ++-- backend/requirements.txt | 11 +++++++++++ docker-compose.yml | 28 ++++++++++++++++++++++++++-- 6 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 backend/Dockerfile create mode 100644 backend/__init__.py create mode 100644 backend/requirements.txt diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..0cca653e --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,13 @@ +# backend/dockerfile + +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt ./requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем всю папку backend в контейнер +COPY . ./backend + +CMD ["uvicorn", "backend.profile.main:app", "--host", "0.0.0.0", "--port", "8080", "--reload"] \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/profile/main.py b/backend/profile/main.py index c9d01fd0..94499c64 100644 --- a/backend/profile/main.py +++ b/backend/profile/main.py @@ -1,4 +1,3 @@ -import warnings from fastapi import FastAPI from backend.profile.routers import profile, project diff --git a/backend/profile/utils.py b/backend/profile/utils.py index c2922a83..93a4323f 100644 --- a/backend/profile/utils.py +++ b/backend/profile/utils.py @@ -12,8 +12,8 @@ async def get_current_user(authorization: Annotated[str, Header(...)]) -> str: token = authorization.removeprefix("Bearer ").strip() - async with httpx.AsyncClient(base_url="http://localhost:8080") as client: - response = await client.post("/api/auth/verify", headers={"Authorization": f"Bearer {token}"}) + async with httpx.AsyncClient() as client: + response = await client.post("http://auth:8080/api/auth/verify", headers={"Authorization": f"Bearer {token}"}) if response.status_code != 200: raise HTTPException(status_code=401, detail="Invalid or expired token") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 00000000..d8b1032d --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,11 @@ +pytest==8.4.1 +fastapi==0.115.12 +python-dotenv==1.0.1 +pytest-asyncio==1.0.0 +SQLAlchemy==2.0.41 +fastapi_users==14.0.1 +fastapi-users-db-sqlalchemy==7.0.0 +asyncpg==0.30.0 +SQLAlchemy==2.0.41 +httpx==0.28.1 +uvicorn[standard] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 58e3d085..85b5a236 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,8 +9,15 @@ services: POSTGRES_PASSWORD: "pgpwd4vcd" volumes: - ./postgres-init:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U vcd -d vcd" ] + interval: 5s + timeout: 5s + retries: 10 + runner: build: ./RunnerNode + auth: build: ./auth ports: @@ -22,11 +29,28 @@ services: POSTGRES_HOST: "postgres" POSTGRES_PORT: 5432 depends_on: - - postgres + postgres: + condition: service_healthy + + profile: + build: ./backend + ports: + - "8081:8080" + environment: + POSTGRES_DB: "vcd" + POSTGRES_USER: "vcd" + POSTGRES_PASSWORD: "pgpwd4vcd" + POSTGRES_HOST: "postgres" + POSTGRES_PORT: 5432 + depends_on: + postgres: + condition: service_healthy + webserver: build: ./UI volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - runner - - auth \ No newline at end of file + - auth + - profile \ No newline at end of file From 40d5f521de698dacfb1cd82d0c70e11d92752a14 Mon Sep 17 00:00:00 2001 From: doshq Date: Sat, 19 Jul 2025 18:30:19 +0300 Subject: [PATCH 101/152] Remove unused imports --- backend/profile/config.py | 15 --------------- backend/profile/database/db.py | 4 ++-- backend/profile/database/pgre_projects.py | 1 - backend/profile/database/pgre_users.py | 7 ++++--- backend/profile/models.py | 6 +++--- backend/profile/routers/profile.py | 2 +- backend/profile/routers/project.py | 2 +- backend/profile/schemas.py | 3 +-- backend/profile/utils.py | 3 +-- 9 files changed, 13 insertions(+), 30 deletions(-) delete mode 100644 backend/profile/config.py diff --git a/backend/profile/config.py b/backend/profile/config.py deleted file mode 100644 index 7097d541..00000000 --- a/backend/profile/config.py +++ /dev/null @@ -1,15 +0,0 @@ -import os -from pathlib import Path -from dotenv import load_dotenv - - -# dotenv_path = Path('.env') -# load_dotenv() -# -# MONGO_URI = os.getenv("MONGO_URI") -# SECRET = os.getenv("SECRET") -# -# if not MONGO_URI: -# raise ValueError("MONGO_URI not set in environment variables") -# if not SECRET: -# raise ValueError("SECRET not set in environment variables") \ No newline at end of file diff --git a/backend/profile/database/db.py b/backend/profile/database/db.py index 5fbfb176..7b8cbc5e 100644 --- a/backend/profile/database/db.py +++ b/backend/profile/database/db.py @@ -1,13 +1,13 @@ import os from typing import Any, AsyncGenerator from fastapi import Depends -from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from sqlalchemy.ext.asyncio.session import AsyncSession from backend.profile.database.pgre_projects import PostgreSQLProjectDatabase from backend.profile.database.pgre_users import PostgreSQLUserDatabase from backend.profile.models import User, ProjectModel -from backend.profile.schemas import UserDB, UserProfile, ProjectDB +from backend.profile.schemas import UserDB, ProjectDB try: DATABASE_URL = ( diff --git a/backend/profile/database/pgre_projects.py b/backend/profile/database/pgre_projects.py index 61f1c5db..b0dc33b3 100644 --- a/backend/profile/database/pgre_projects.py +++ b/backend/profile/database/pgre_projects.py @@ -1,5 +1,4 @@ from typing import Any, Dict, Optional, Type, TypeVar -from fastapi_users.db.base import BaseUserDatabase from sqlalchemy import select, delete, update, insert from sqlalchemy.ext.asyncio import AsyncSession from backend.profile.models import ProjectModel diff --git a/backend/profile/database/pgre_users.py b/backend/profile/database/pgre_users.py index b750a334..c15a7c07 100644 --- a/backend/profile/database/pgre_users.py +++ b/backend/profile/database/pgre_users.py @@ -1,9 +1,9 @@ +from pydantic import EmailStr from typing import Any, Dict, Optional, Type, TypeVar from fastapi_users.db.base import BaseUserDatabase -from pydantic import EmailStr from sqlalchemy import select, delete, update, insert from sqlalchemy.ext.asyncio import AsyncSession -from backend.profile.models import User as UserModel # SQLAlchemy модель +from backend.profile.models import User as UserModel from backend.profile.schemas import UserDB UP = TypeVar("UP", bound=UserDB) @@ -83,7 +83,8 @@ def _convert_to_userdb(self, user: UserModel) -> UP: "hashed_password": user.password_hash, "created_at": user.created_at, "salt": user.salt, - # Добавляем обязательные поля со значениями по умолчанию + + # Add default required fields "is_active": True, "is_superuser": False, "is_verified": True diff --git a/backend/profile/models.py b/backend/profile/models.py index ef10940d..fad5e017 100644 --- a/backend/profile/models.py +++ b/backend/profile/models.py @@ -3,11 +3,11 @@ from sqlalchemy.orm import Mapped, mapped_column, declarative_base from sqlalchemy.sql.schema import Column, ForeignKey -Base = declarative_base() +Base = declarative_base() class User(Base): - __tablename__ = "users" # Изменяем на множественное число + __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True) username: Mapped[str] = mapped_column(String(50), nullable=False) @@ -15,7 +15,7 @@ class User(Base): email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) created_at = Column(TIMESTAMP(timezone=True), server_default=text('now()'), nullable=False) password_hash: Mapped[str] = mapped_column("password_hash", String(length=1024), - nullable=False) # Сопоставление с БД + nullable=False) salt: Mapped[str] = mapped_column(String(64), nullable=False) # @property diff --git a/backend/profile/routers/profile.py b/backend/profile/routers/profile.py index ce61e31c..17b078ee 100644 --- a/backend/profile/routers/profile.py +++ b/backend/profile/routers/profile.py @@ -1,10 +1,10 @@ -from uuid import UUID from fastapi import APIRouter, HTTPException, Depends from backend.profile.database.db import get_user_db from backend.profile.database.pgre_users import PostgreSQLUserDatabase from backend.profile.schemas import UpdateName, UpdateEmail, UpdatePassword, UserDB from backend.profile.utils import get_current_user + router = APIRouter(prefix="/api/profile", tags=["profile"]) @router.get("/{id}") diff --git a/backend/profile/routers/project.py b/backend/profile/routers/project.py index 31b89747..297c2234 100644 --- a/backend/profile/routers/project.py +++ b/backend/profile/routers/project.py @@ -4,8 +4,8 @@ from backend.profile.schemas import Project, ProjectDB, UserDB, ProjectCreateResponse from backend.profile.utils import get_current_user -router = APIRouter(prefix="/api/profile/{id}/project", tags=["projects"]) +router = APIRouter(prefix="/api/profile/{id}/project", tags=["projects"]) @router.post("", response_model=ProjectCreateResponse) async def create_project( diff --git a/backend/profile/schemas.py b/backend/profile/schemas.py index d26a76d7..11234579 100644 --- a/backend/profile/schemas.py +++ b/backend/profile/schemas.py @@ -1,8 +1,7 @@ from pydantic import EmailStr from datetime import datetime from typing import Optional, Dict -from uuid import UUID -from fastapi_users import schemas, models +from fastapi_users import schemas # Users schemas diff --git a/backend/profile/utils.py b/backend/profile/utils.py index 93a4323f..1bb3c7c5 100644 --- a/backend/profile/utils.py +++ b/backend/profile/utils.py @@ -2,8 +2,7 @@ import httpx import base64 from typing import Annotated -from fastapi import Depends, HTTPException, Header -from fastapi.security import OAuth2PasswordBearer +from fastapi import HTTPException, Header async def get_current_user(authorization: Annotated[str, Header(...)]) -> str: From 88a26c1c553bcac63345dcf053c275f70c0da1d5 Mon Sep 17 00:00:00 2001 From: arsenez Date: Sat, 19 Jul 2025 22:56:00 +0300 Subject: [PATCH 102/152] Add explicit rule for files with an extension --- nginx.conf | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nginx.conf b/nginx.conf index 8415b0f9..5774a0a4 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,6 +1,7 @@ server { listen 80 default_server; server_name _; + root /usr/share/nginx/html; location /socket.io { proxy_set_header X-Real-IP $remote_addr; @@ -22,8 +23,11 @@ server { proxy_set_header X-Real-IP $remote_addr; } + location ~ \.[A-Za-z0-9]+$ { + try_files $uri = 404; + } + location / { - root /usr/share/nginx/html; try_files $uri /index.html; } } From 0413a8d2b3f86c162e5ec12953ce021d3591e027 Mon Sep 17 00:00:00 2001 From: doshq Date: Sat, 19 Jul 2025 23:21:49 +0300 Subject: [PATCH 103/152] Fix launching unit tests with Docker setup --- backend/profile/utils.py | 6 ++- backend/tests/profile_tests/test_delete.py | 59 ++++++++++++++++++++++ docker-compose.yml | 1 + 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 backend/tests/profile_tests/test_delete.py diff --git a/backend/profile/utils.py b/backend/profile/utils.py index 1bb3c7c5..df3b6f40 100644 --- a/backend/profile/utils.py +++ b/backend/profile/utils.py @@ -1,3 +1,4 @@ +import os import json import httpx import base64 @@ -5,14 +6,17 @@ from fastapi import HTTPException, Header +AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", "http://localhost:8080") + async def get_current_user(authorization: Annotated[str, Header(...)]) -> str: if not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing Bearer token") token = authorization.removeprefix("Bearer ").strip() + verify_url = f"{AUTH_SERVICE_URL}/api/auth/verify" async with httpx.AsyncClient() as client: - response = await client.post("http://auth:8080/api/auth/verify", headers={"Authorization": f"Bearer {token}"}) + response = await client.post(verify_url, headers={"Authorization": f"Bearer {token}"}) if response.status_code != 200: raise HTTPException(status_code=401, detail="Invalid or expired token") diff --git a/backend/tests/profile_tests/test_delete.py b/backend/tests/profile_tests/test_delete.py new file mode 100644 index 00000000..fc21830d --- /dev/null +++ b/backend/tests/profile_tests/test_delete.py @@ -0,0 +1,59 @@ +import pytest + +from backend.profile.utils import decode_jwt +from fastapi import status + + +@pytest.mark.asyncio +async def test_create_project(auth_client, registered_user, profile_client, test_project_data): + original_user, original_token = registered_user + user_data = { + "name": "some user", + "username": "deleteuser", + "email": "deleteme@yandex.ru", + "password": "qwerty12345", + } + response = await auth_client.post("/api/auth/register", json=user_data) + assert response.status_code == 201 or response.status_code == 409 + + login_response = await auth_client.post( + "/api/auth/login", + json={ + "login": user_data["username"], + "password": user_data["password"] + } + ) + token = login_response.json().get("access") + id = decode_jwt(token)['id'] + + creation_project_response = await profile_client.post( + f"/api/profile/{id}/project", + headers={"Authorization": f"Bearer {token}"}, + json=test_project_data + ) + + assert creation_project_response.status_code == 200 + pid = creation_project_response.json()["project_id"] + + delete_project_response = await profile_client.delete( + f"/api/profile/{id}/project/{pid}", + headers={"Authorization": f"Bearer {token}"} + ) + + assert delete_project_response.status_code == status.HTTP_200_OK + assert delete_project_response.status_code == 200 + + delete_user_response = await profile_client.delete( + f"/api/profile/{id}", + headers={"Authorization": f"Bearer {token}"} + ) + + assert delete_user_response.status_code == status.HTTP_200_OK + assert delete_user_response.json()['status'] == 'deleted' + + response = await profile_client.get( + f"/api/profile/{id}", + headers={"Authorization": f"Bearer {token}"} + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND diff --git a/docker-compose.yml b/docker-compose.yml index 85b5a236..c4b877a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,7 @@ services: ports: - "8081:8080" environment: + AUTH_SERVICE_URL: "http://auth:8080" POSTGRES_DB: "vcd" POSTGRES_USER: "vcd" POSTGRES_PASSWORD: "pgpwd4vcd" From 5564b79d4b4b744043d65d2fc1fe0655980bc7d9 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 00:05:47 +0300 Subject: [PATCH 104/152] creation of custom circuits moved to the context menu --- .../codeComponents/PaneContextMenu.jsx | 82 +++---- UI/src/components/pages/mainPage.jsx | 27 ++- .../pages/mainPage/circuitsMenu.jsx | 2 +- .../pages/mainPage/createCustomBlockModal.jsx | 78 +++++++ .../pages/mainPage/customCircuit.jsx | 200 ------------------ UI/src/components/utils/customBlockUtils.js | 82 +++++++ 6 files changed, 210 insertions(+), 261 deletions(-) create mode 100644 UI/src/components/pages/mainPage/createCustomBlockModal.jsx delete mode 100644 UI/src/components/pages/mainPage/customCircuit.jsx create mode 100644 UI/src/components/utils/customBlockUtils.js diff --git a/UI/src/components/codeComponents/PaneContextMenu.jsx b/UI/src/components/codeComponents/PaneContextMenu.jsx index 8acc51fd..1428bc4b 100644 --- a/UI/src/components/codeComponents/PaneContextMenu.jsx +++ b/UI/src/components/codeComponents/PaneContextMenu.jsx @@ -2,59 +2,33 @@ import React, { useCallback } from "react"; import { useReactFlow } from "@xyflow/react"; export default function PaneContextMenu({ - copyElements, - pasteElements, - cutElements, - selectedElements, - clipboard, - onClose, - top, - left, - right, - bottom, - ...props -}) { + copyElements, + pasteElements, + cutElements, + selectedElements, + clipboard, + onClose, + top, + left, + right, + bottom, + onAddCustomCircuit, // New prop to open modal + ...props + }) { const { setNodes, setEdges } = useReactFlow(); - // const rotateSelectedNodes = useCallback( - // (angle) => { - // console.log("selectedElements", selectedElements); - // if (!selectedElements?.nodes?.length) return; - // const selectedNodeIds = new Set(selectedElements.nodes.map((n) => n.id)); - // - // setNodes((nodes) => - // nodes.map((node) => { - // if (selectedNodeIds.has(node.id)) { - // const currentRotation = node.data?.rotation || 0; - // const newRotation = (currentRotation + angle + 360) % 360; - // return { - // ...node, - // data: { - // ...node.data, - // rotation: newRotation, - // }, - // }; - // } - // return node; - // }) - // ); - // }, - // [selectedElements, setNodes], - // ); - const deleteSelectedElements = useCallback(() => { const selectedNodeIds = new Set(selectedElements.nodes.map((n) => n.id)); const selectedEdgeIds = new Set(selectedElements.edges.map((e) => e.id)); setNodes((nodes) => nodes.filter((node) => !selectedNodeIds.has(node.id))); - setEdges((edges) => edges.filter( (edge) => !selectedEdgeIds.has(edge.id) && !selectedNodeIds.has(edge.source) && - !selectedNodeIds.has(edge.target), - ), + !selectedNodeIds.has(edge.target) + ) ); }, [selectedElements, setNodes, setEdges]); @@ -66,36 +40,40 @@ export default function PaneContextMenu({ {...props} > + + + {/* New custom circuit button */}
); -} +} \ No newline at end of file diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 8fe79763..1d54efdb 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -66,7 +66,7 @@ import { redo as redoUtil } from "../utils/redo.js"; import { handleTabSwitch as handleTabSwitchUtil } from "../utils/handleTabSwitch.js"; import { getEditableNode } from "../utils/getEditableNode.js"; import { handleNameChange } from "../utils/handleNameChange.js"; -import CreateCustomBlock from "./mainPage/customCircuit.jsx"; +import CreateCustomBlockModal from "./mainPage/CreateCustomBlockModal.jsx"; export const SimulateStateContext = createContext({ simulateState: "idle", @@ -138,6 +138,18 @@ export default function Main() { const ignoreChangesRef = useRef(false); + const [modalOpen, setModalOpen] = useState(false); + + const handleCreateFromCurrent = (customBlock) => { + // Handle custom block creation + console.log("Created custom block:", customBlock); + }; + + const handleCreateFromFile = () => { + // Handle file import logic + console.log("Create from file"); + }; + const editableNode = useMemo( () => getEditableNode(nodes, edges), [nodes, edges], @@ -691,6 +703,7 @@ export default function Main() { cutElements={cutElements} onClose={closeMenu} clipboard={clipboard} + onAddCustomCircuit={() => setModalOpen(true)} /> )} @@ -760,15 +773,13 @@ export default function Main() { }} /> - setModalOpen(false)} nodes={nodes} edges={edges} - onCreateFromFile={() => { - console.log("создать из файла"); - }} - onCreateFromCurrent={() => { - console.log("создать из текущего"); - }} + onCreateFromFile={handleCreateFromFile} + onCreateFromCurrent={handleCreateFromCurrent} /> { diff --git a/UI/src/components/pages/mainPage/createCustomBlockModal.jsx b/UI/src/components/pages/mainPage/createCustomBlockModal.jsx new file mode 100644 index 00000000..51b823a1 --- /dev/null +++ b/UI/src/components/pages/mainPage/createCustomBlockModal.jsx @@ -0,0 +1,78 @@ +import React, { useState } from "react"; +import { IconCloseCross } from "../../../../assets/ui-icons.jsx"; +import { showToastError } from "../../codeComponents/logger.jsx"; +import { createCustomBlock, saveCustomBlock } from "../../utils/customBlockUtils"; + +export default function CreateCustomBlockModal({ + isOpen, + onClose, + nodes, + edges, + onCreateFromFile, + onCreateFromCurrent, + }) { + const [blockName, setBlockName] = useState(""); + const [error, setError] = useState(""); + + const handleCreateFromCurrent = () => { + if (!blockName.trim()) { + showToastError("Please enter a custom block name."); + return; + } + + try { + const customBlock = createCustomBlock(nodes, edges, blockName.trim()); + saveCustomBlock(customBlock); + + setBlockName(""); + setError(""); + onClose(); + + if (onCreateFromCurrent) onCreateFromCurrent(customBlock); + alert(`Block "${blockName}" created successfully!`); + } catch (err) { + console.error("Error creating block:", err); + setError(`Error: ${err.message}`); + } + }; + + const handleCreateFromFile = () => { + onClose(); + if (onCreateFromFile) onCreateFromFile(); + }; + + if (!isOpen) return null; + + return ( +
+
+

Create custom block

+ + +
+ + +
+ +
+ setBlockName(e.target.value)} + placeholder="New custom block name" + required + /> + {error &&

{error}

} +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/UI/src/components/pages/mainPage/customCircuit.jsx b/UI/src/components/pages/mainPage/customCircuit.jsx deleted file mode 100644 index 18943356..00000000 --- a/UI/src/components/pages/mainPage/customCircuit.jsx +++ /dev/null @@ -1,200 +0,0 @@ -import React, { useState } from "react"; -import { IconCloseCross } from "../../../../assets/ui-icons.jsx"; -import toast from "react-hot-toast"; -import { showToastError } from "../../codeComponents/logger.jsx"; - -export default function CreateCustomBlock({ - nodes, - edges, - onCreateFromFile, // Колбэк при создании из файла (вы реализуете) - onCreateFromCurrent, // Колбэк при создании из схемы (вы реализуете) -}) { - const [isModalOpen, setIsModalOpen] = useState(false); - const [blockName, setBlockName] = useState(""); - const [error, setError] = useState(""); - - const handleCreateFromCurrent = () => { - if (!blockName.trim()) { - showToastError("Please enter a custom block name."); - return; - } - - try { - // Создаем кастомный блок из текущей схемы - const customBlock = createCustomBlock(nodes, edges, blockName.trim()); - - // Сохраняем в localStorage - saveCustomBlock(customBlock); - - // Сбрасываем состояние - setBlockName(""); - setError(""); - setIsModalOpen(false); - - // Вызываем колбэк (если нужна дополнительная логика) - if (onCreateFromCurrent) { - onCreateFromCurrent(customBlock); - } - - alert(`Блок "${blockName}" успешно создан!`); - } catch (err) { - console.error("Ошибка при создании блока:", err); - setError(`Ошибка: ${err.message}`); - } - }; - - const handleCreateFromFile = () => { - setIsModalOpen(false); - if (onCreateFromFile) { - onCreateFromFile(); - } - }; - - return ( -
- - - {isModalOpen && ( -
-
-

Create custom block

- - - -
- - -
- - -
- setBlockName(e.target.value)} - placeholder="New custom block name" - required - /> - {error &&

{error}

} -
-
-
-
-
- )} -
- ); -} - -const generateCustomBlockId = () => { - const timestamp = Date.now().toString(36); - const random = Math.random().toString(36).substring(2, 10); - return `custom_${timestamp}_${random}`; -}; - -export const createCustomBlock = (nodes, edges, blockName) => { - // Валидация входных данных - if (!Array.isArray(nodes)) { - throw new Error("Invalid nodes: must be an array"); - } - - if (!Array.isArray(edges)) { - throw new Error("Invalid edges: must be an array"); - } - - // Фильтрация входных нод - const inputs = nodes.reduce((acc, node) => { - if (node.type === "inputNodeSwitch" || node.type === "inputNodeButton") { - acc.push({ - id: node.id, - name: node.name || `input_${Math.floor(Math.random() * 10000)}`, // Случайный номер - }); - } - return acc; - }, []); - - // Фильтрация выходных нод - const outputs = nodes.reduce((acc, node) => { - if (node.type === "outputNodeLed") { - acc.push({ - id: node.id, - name: node.name || `output_${Math.floor(Math.random() * 10000)}`, // Случайный номер - }); - } - return acc; - }, []); - - return { - id: generateCustomBlockId(), - name: blockName, - inputs, - outputs, - originalSchema: { nodes, edges }, // Сохраняем полную схему - }; -}; - -/** - * Сохраняет кастомный блок в localStorage - */ -export const saveCustomBlock = (customBlock) => { - try { - const savedBlocks = JSON.parse( - localStorage.getItem("customBlocks") || "[]", - ); - const updatedBlocks = [...savedBlocks, customBlock]; - localStorage.setItem("customBlocks", JSON.stringify(updatedBlocks)); - } catch (error) { - console.error("Failed to save custom block:", error); - } -}; - -/** - * Загружает все кастомные блоки из localStorage - */ -export const loadCustomBlocks = () => { - try { - return JSON.parse(localStorage.getItem("customBlocks") || "[]"); - } catch (error) { - console.error("Failed to load custom blocks:", error); - return []; - } -}; - -/** - * Удаляет кастомный блок по ID - */ -export const deleteCustomBlock = (blockId) => { - try { - const savedBlocks = JSON.parse( - localStorage.getItem("customBlocks") || "[]", - ); - const updatedBlocks = savedBlocks.filter((block) => block.id !== blockId); - localStorage.setItem("customBlocks", JSON.stringify(updatedBlocks)); - return true; - } catch (error) { - console.error("Failed to delete custom block:", error); - return false; - } -}; - -/** - * Находит кастомный блок по ID - */ -export const findCustomBlockById = (blockId) => { - const blocks = loadCustomBlocks(); - return blocks.find((block) => block.id === blockId); -}; diff --git a/UI/src/components/utils/customBlockUtils.js b/UI/src/components/utils/customBlockUtils.js new file mode 100644 index 00000000..aa48e8a4 --- /dev/null +++ b/UI/src/components/utils/customBlockUtils.js @@ -0,0 +1,82 @@ +/*TODO: Add tests*/ +export const generateCustomBlockId = () => { + const timestamp = Date.now().toString(36); + const random = Math.random().toString(36).substring(2, 10); + return `custom_${timestamp}_${random}`; +}; + +export const createCustomBlock = (nodes, edges, blockName) => { + if (!Array.isArray(nodes)) throw new Error("Invalid nodes: must be an array"); + if (!Array.isArray(edges)) throw new Error("Invalid edges: must be an array"); + + const inputs = nodes.filter(node => + node.type === "inputNodeSwitch" || node.type === "inputNodeButton" + ).map(node => ({ + id: node.id, + name: node.name || `input_${Math.floor(Math.random() * 10000)}`, + })); + + const outputs = nodes.filter(node => + node.type === "outputNodeLed" + ).map(node => ({ + id: node.id, + name: node.name || `output_${Math.floor(Math.random() * 10000)}`, + })); + + return { + id: generateCustomBlockId(), + name: blockName, + inputs, + outputs, + originalSchema: { nodes, edges }, + }; +}; + +/** + * Сохраняет кастомный блок в localStorage + */ +export const saveCustomBlock = (customBlock) => { + try { + const savedBlocks = JSON.parse(localStorage.getItem("customBlocks") || "[]"); + localStorage.setItem("customBlocks", JSON.stringify([...savedBlocks, customBlock])); + } catch (error) { + console.error("Failed to save custom block:", error); + } +}; + +/** + * Загружает все кастомные блоки из localStorage + */ +export const loadCustomBlocks = () => { + try { + return JSON.parse(localStorage.getItem("customBlocks") || "[]"); + } catch (error) { + console.error("Failed to load custom blocks:", error); + return []; + } +}; + +/** + * Удаляет кастомный блок по ID + */ +export const deleteCustomBlock = (blockId) => { + try { + const savedBlocks = JSON.parse( + localStorage.getItem("customBlocks") || "[]", + ); + const updatedBlocks = savedBlocks.filter((block) => block.id !== blockId); + localStorage.setItem("customBlocks", JSON.stringify(updatedBlocks)); + return true; + } catch (error) { + console.error("Failed to delete custom block:", error); + return false; + } +}; + +/** + * Находит кастомный блок по ID + */ +export const findCustomBlockById = (blockId) => { + const blocks = loadCustomBlocks(); + return blocks.find((block) => block.id === blockId); +}; From 1359d834cb9e9dcd649a24aec737b31b0f17c06f Mon Sep 17 00:00:00 2001 From: witch2256 Date: Sun, 20 Jul 2025 00:07:15 +0300 Subject: [PATCH 105/152] login and registration API connected with backend --- UI/src/CSS/auth.css | 8 +- UI/src/CSS/reg.css | 23 +++- UI/src/components/pages/auth.jsx | 122 ++++++++++------- UI/src/components/pages/register.jsx | 76 ++++++++++- auth/src/api/AuthRequestHandlerFactory.cpp | 146 ++++++++++++++++++--- docker-compose.yml | 11 +- 6 files changed, 317 insertions(+), 69 deletions(-) diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index df0074c2..3c6be920 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -67,6 +67,12 @@ animation: fadeIn 0.3s ease-in; } +.server-error-message { + text-align: center; + margin-top: 0.5rem; + color: #ff4d4f; +} + .input-password-window { display: flex; align-items: center; @@ -109,7 +115,7 @@ .register-text { font-size: 1.1rem; font-family: Montserrat, serif; - margin-top: 5rem; + margin-top: 4.5rem; color: var(--main-0); } diff --git a/UI/src/CSS/reg.css b/UI/src/CSS/reg.css index d6d386e1..8f9f7e9a 100644 --- a/UI/src/CSS/reg.css +++ b/UI/src/CSS/reg.css @@ -10,7 +10,7 @@ .reg-window { position: fixed; width: 30rem; - height: 40rem; + height: 50rem; background-color: var(--main-1); border-radius: 0.5rem; border: var(--main-5) solid 0.05rem; @@ -22,6 +22,25 @@ color: var(--main-0); } +.input-name-text { + margin-top: 1.5rem; + font-size: 1rem; + display: flex; + color: var(--main-0); +} + +.input-name-window { + margin-top: 0.7rem; + height: 2rem; + width: 25rem; + font-size: 0.9rem; + border: var(--main-5) solid 0.05rem; + border-radius: 0.5rem; + font-family: Montserrat, serif; + background-color: var(--main-2); + color: var(--main-0); +} + .input-username-text { margin-top: 1.5rem; font-size: 1rem; @@ -104,7 +123,7 @@ } .reg-button { - margin-top: 8rem; + margin-top: 17rem; height: 2rem; width: 25rem; display: flex; diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index ea7bc4a7..31abfee0 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -1,35 +1,28 @@ import React, { useState, useEffect, useCallback } from "react"; import "../../CSS/auth.css"; import "../../CSS/variables.css"; -import { Link, useNavigate } from "react-router-dom"; // Добавлен useNavigate - -const EMAIL_REGEXP = - /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; +import { Link, useNavigate } from "react-router-dom"; const Auth = () => { - const navigate = useNavigate(); // Используем хук + const navigate = useNavigate(); - // Состояния полей формы - const [email, setEmail] = useState(""); + const [login, setLogin] = useState(""); const [password, setPassword] = useState(""); - // Состояния валидации - const [emailError, setEmailError] = useState(""); - const [wasEmailFocused, setWasEmailFocused] = useState(false); + const [loginError, setLoginError] = useState(""); + const [wasLoginFocused, setWasLoginFocused] = useState(false); const [passwordError, setPasswordError] = useState(""); const [wasPasswordFocused, setWasPasswordFocused] = useState(false); - // Функции валидации - const validateEmail = useCallback(() => { - if (email.trim() === "") { - return "Email is required"; - } - if (!EMAIL_REGEXP.test(email)) { - return "Please enter a valid email address"; + const [serverError, setServerError] = useState(""); + + const validateLogin = useCallback(() => { + if (login.trim() === "") { + return "Login is required"; } return ""; - }, [email]); + }, [login]); const validatePassword = useCallback(() => { if (password.trim() === "") { @@ -39,15 +32,15 @@ const Auth = () => { }, [password]); // Обработчики изменений - const handleEmailChange = (e) => setEmail(e.target.value); + const handleLoginChange = (e) => setLogin(e.target.value); const handlePasswordChange = (e) => setPassword(e.target.value); // Эффекты валидации useEffect(() => { - if (wasEmailFocused) { - setEmailError(validateEmail()); + if (wasLoginFocused) { + setLoginError(validateLogin()); } - }, [email, wasEmailFocused, validateEmail]); + }, [login, wasLoginFocused, validateLogin]); useEffect(() => { if (wasPasswordFocused) { @@ -56,19 +49,44 @@ const Auth = () => { }, [password, wasPasswordFocused, validatePassword]); // Обработчик входа - const handleLogin = () => { - // Активируем проверку всех полей - setWasEmailFocused(true); + const handleLogin = async () => { + setWasLoginFocused(true); setWasPasswordFocused(true); + setLoginError(""); + setPasswordError(""); + setServerError(""); - const emailErr = validateEmail(); + const loginErr = validateLogin(); const passwordErr = validatePassword(); - setEmailError(emailErr); + setLoginError(loginErr); setPasswordError(passwordErr); - if (!emailErr && !passwordErr) { - navigate("/profile"); + if (loginErr || passwordErr) { + return; + } + try { + const response = await fetch('http://localhost:8080/api/auth/login', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({login, password}), + }); + + if (response.ok) { + const data = await response.json(); + localStorage.setItem('accessToken', data.access); + localStorage.setItem('refreshToken', data.refresh); + navigate('/profile'); + } else if (response.status === 401) { + const errorText = await response.text(); + setServerError(errorText || "Invalid login or password"); + } else { + const errorText = await response.text(); + setServerError(errorText || "Authorization failed"); + } + } catch (error) { + console.error('Login error:', error); + setServerError("Network error. Please try again."); } }; @@ -76,31 +94,29 @@ const Auth = () => {
Log in
+
-
Enter email:
+
Login
setWasEmailFocused(false)} + className={`input-email-window ${wasLoginFocused && loginError ? "invalid" : ""}`} + value={login} + onChange={handleLoginChange} + onFocus={() => setWasLoginFocused(false)} onBlur={() => { - if (email.trim() !== "") { - setWasEmailFocused(true); + if (login.trim() !== "") { + setWasLoginFocused(true); } }} - placeholder="myEmail@example.com" + placeholder="email or username" /> - {wasEmailFocused && emailError && ( -
{emailError}
+ {/* Ошибки валидации и не-401 ошибки сервера */} + {wasLoginFocused && loginError && !serverError && ( +
{loginError}
)} -
Enter password:
+
Password:
{ )}
- + {serverError && ( +
{serverError}
+ )}
Have no account?
@@ -129,4 +155,4 @@ const Auth = () => { ); }; -export default Auth; +export default Auth; \ No newline at end of file diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index 54ab0f49..aff4011e 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -10,12 +10,16 @@ const Auth = () => { const navigate = useNavigate(); // Состояния полей формы + const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [username, setUsername] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); // Состояния валидации + const [nameError, setNameError] = useState(""); + const [wasNameFocused, setWasNameFocused] = useState(false); + const [emailError, setEmailError] = useState(""); const [wasEmailFocused, setWasEmailFocused] = useState(false); @@ -30,6 +34,19 @@ const Auth = () => { useState(false); // Функции валидации + const validateName = useCallback(() => { + if (name.trim() === "") { + return "Name is required"; + } + if (name.length < 2 || name.length > 52) { + return "Name length must be 2-52 characters"; + } + if (!/^[a-zA-Z]*$/.test(name)) { + return "Name contains invalid character"; + } + return ""; + }, [name]); + const validateUsername = useCallback(() => { if (username.trim() === "") { return "Username is required"; @@ -77,6 +94,7 @@ const Auth = () => { }, [confirmPassword, password]); // Обработчики изменений + const handleNameChange = (e) => setName(e.target.value); const handleUsernameChange = (e) => setUsername(e.target.value); const handleEmailChange = (e) => setEmail(e.target.value); const handlePasswordChange = (e) => setPassword(e.target.value); @@ -84,6 +102,10 @@ const Auth = () => { // Эффект валидации useEffect(() => { + if (wasNameFocused) { + setNameError(validateName()); + } + if (wasUsernameFocused) { setUsernameError(validateUsername()); } @@ -100,6 +122,8 @@ const Auth = () => { setConfirmPasswordError(validateConfirmPassword()); } }, [ + name, + wasNameFocused, username, wasUsernameFocused, email, @@ -108,6 +132,7 @@ const Auth = () => { wasPasswordFocused, confirmPassword, wasConfirmPasswordFocused, + validateName, validateUsername, validateEmail, validatePassword, @@ -115,27 +140,56 @@ const Auth = () => { ]); // Обработчик регистрации - const handleRegister = () => { + const handleRegister = async () => { + setWasNameFocused(true); setWasUsernameFocused(true); setWasEmailFocused(true); setWasPasswordFocused(true); setWasConfirmPasswordFocused(true); const errors = { + name: validateName(), username: validateUsername(), email: validateEmail(), password: validatePassword(), confirmPassword: validateConfirmPassword(), }; + setNameError(errors.name); setUsernameError(errors.username); setEmailError(errors.email); setPasswordError(errors.password); setConfirmPasswordError(errors.confirmPassword); const hasErrors = Object.values(errors).some((error) => error !== ""); + if (!hasErrors) { - navigate("/profile"); + try { + const response = await fetch('http://localhost:8080/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name, + username, + email, + password, + }), + }); + if (response.ok) { + navigate('/profile'); + } else if (response.status === 409) { + const errorText = await response.text(); + if (errorText === 'username exists') { + setUsernameError(errorText); + } else if (errorText === 'email exists') { + setEmailError(errorText); + } + } else { + throw new Error('Registration failed'); + } + } catch (error) { + console.error(error); + } } }; @@ -144,6 +198,24 @@ const Auth = () => {
Registration
+ {/*Поле для имени*/} +
Name
+ setWasNameFocused(false)} + onBlur={() => { + if (name.trim() !== "") { + setWasNameFocused(true); + } + }} + /> + {wasNameFocused && nameError && ( +
{nameError}
+ )} {/* Поле username */}
Username
#include +#include using Poco::Net::HTTPRequestHandler; using Poco::Net::HTTPServerRequest; +using Poco::Net::HTTPServerResponse; + +class BaseCORSHandler : public Poco::Net::HTTPRequestHandler { +protected: + void addCORSHeaders(Poco::Net::HTTPServerResponse& response) { + response.set("Access-Control-Allow-Origin", "http://localhost:5173"); + response.set("Access-Control-Allow-Methods", "POST, OPTIONS"); + response.set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With"); + response.set("Access-Control-Max-Age", "86400"); + response.set("Vary", "Origin"); + } +}; + +class CORSHandler : public BaseCORSHandler { +public: + void handleRequest(Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response) override { + addCORSHeaders(response); + if (request.getMethod() == "OPTIONS") { + response.setStatus(Poco::Net::HTTPResponse::HTTP_OK); + response.send(); + } else { + response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED); + response.send(); + } + } +}; + +class CORSHandlerWrapper : public BaseCORSHandler { +public: + CORSHandlerWrapper(Poco::Net::HTTPRequestHandler* handler) + : m_handler(handler) {} + + ~CORSHandlerWrapper() override { + delete m_handler; + } + + void handleRequest(Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response) override { + addCORSHeaders(response); + m_handler->handleRequest(request, response); + } + +private: + Poco::Net::HTTPRequestHandler* m_handler; +}; + +class CORSRegistrationHandler : public BaseCORSHandler { +public: + explicit CORSRegistrationHandler(DBConnector& db) : m_handler(db) {} + + void handleRequest(Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response) override { + addCORSHeaders(response); + m_handler.handleRequest(request, response); + } +private: + RegistrationHandler m_handler; +}; + +class CORSLoginHandler : public BaseCORSHandler { +public: + CORSLoginHandler(DBConnector& db, TokenManager& tokenManager) + : m_handler(db, tokenManager) {} + + void handleRequest(Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response) override { + addCORSHeaders(response); + m_handler.handleRequest(request, response); + } +private: + LoginHandler m_handler; +}; + +class CORSVerificationHandler : public BaseCORSHandler { +public: + explicit CORSVerificationHandler(TokenManager& tokenManager) + : m_handler(tokenManager) {} + + void handleRequest(Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response) override { + addCORSHeaders(response); + m_handler.handleRequest(request, response); + } +private: + VerificationHandler m_handler; +}; + +class CORSRefreshHandler : public BaseCORSHandler { +public: + explicit CORSRefreshHandler(TokenManager& tokenManager) + : m_handler(tokenManager) {} + + void handleRequest(Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response) override { + addCORSHeaders(response); + m_handler.handleRequest(request, response); + } +private: + RefreshHandler m_handler; +}; + +class CORSNotFoundHandler : public BaseCORSHandler { +public: + void handleRequest(Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response) override { + addCORSHeaders(response); + response.setStatus(Poco::Net::HTTPResponse::HTTP_NOT_FOUND); + response.send(); + } +}; AuthRequestHandlerFactory::AuthRequestHandlerFactory( DBConnector& db, TokenManager& tokenManager @@ -21,18 +133,22 @@ AuthRequestHandlerFactory::AuthRequestHandlerFactory( , m_tokenManager(tokenManager) {} HTTPRequestHandler* -AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request -) { - if (request.getMethod() == "POST") { - if (request.getURI() == "/api/auth/register") { - return new RegistrationHandler(m_db); - } else if (request.getURI() == "/api/auth/login") { - return new LoginHandler(m_db, m_tokenManager); - } else if (request.getURI() == "/api/auth/verify") { - return new VerificationHandler(m_tokenManager); - } else if (request.getURI() == "/api/auth/refresh") { - return new RefreshHandler(m_tokenManager); - } - } - return new NotFoundHandler; -} +AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request) { + if (request.getMethod() == "OPTIONS") { + return new CORSHandler(); + } + + if (request.getMethod() == "POST") { + if (request.getURI() == "/api/auth/register") { + return new CORSRegistrationHandler(m_db); + } else if (request.getURI() == "/api/auth/login") { + return new CORSLoginHandler(m_db, m_tokenManager); + } else if (request.getURI() == "/api/auth/verify") { + return new CORSVerificationHandler(m_tokenManager); + } else if (request.getURI() == "/api/auth/refresh") { + return new CORSRefreshHandler(m_tokenManager); + } + } + + return new CORSNotFoundHandler(); +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index fca9675f..9422d00a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,18 +7,27 @@ services: POSTGRES_PASSWORD: "pgpwd4vcd" volumes: - ./postgres-init:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U vcd -d vcd" ] + interval: 5s + timeout: 5s + retries: 10 runner: build: ./RunnerNode auth: build: ./auth + ports: + - "8080:8080" environment: + PORT: 8080 POSTGRES_DB: "vcd" POSTGRES_USER: "vcd" POSTGRES_PASSWORD: "pgpwd4vcd" POSTGRES_HOST: "postgres" POSTGRES_PORT: 5432 depends_on: - - postgres + postgres: + condition: service_healthy webserver: build: ./UI volumes: From 1d1dd01858f47551886e4c525e4a013e79d8683d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:07:58 +0000 Subject: [PATCH 106/152] Automated formatting --- UI/src/components/pages/auth.jsx | 29 ++-- UI/src/components/pages/register.jsx | 31 ++-- auth/src/api/AuthRequestHandlerFactory.cpp | 185 ++++++++++++--------- 3 files changed, 131 insertions(+), 114 deletions(-) diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index 31abfee0..b2f6ef36 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -66,17 +66,17 @@ const Auth = () => { return; } try { - const response = await fetch('http://localhost:8080/api/auth/login', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({login, password}), + const response = await fetch("http://localhost:8080/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ login, password }), }); if (response.ok) { const data = await response.json(); - localStorage.setItem('accessToken', data.access); - localStorage.setItem('refreshToken', data.refresh); - navigate('/profile'); + localStorage.setItem("accessToken", data.access); + localStorage.setItem("refreshToken", data.refresh); + navigate("/profile"); } else if (response.status === 401) { const errorText = await response.text(); setServerError(errorText || "Invalid login or password"); @@ -85,7 +85,7 @@ const Auth = () => { setServerError(errorText || "Authorization failed"); } } catch (error) { - console.error('Login error:', error); + console.error("Login error:", error); setServerError("Network error. Please try again."); } }; @@ -132,15 +132,8 @@ const Auth = () => { )}
- - - {serverError && (
{serverError}
@@ -155,4 +148,4 @@ const Auth = () => { ); }; -export default Auth; \ No newline at end of file +export default Auth; diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index aff4011e..811beafb 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -165,27 +165,30 @@ const Auth = () => { if (!hasErrors) { try { - const response = await fetch('http://localhost:8080/api/auth/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name, - username, - email, - password, - }), - }); + const response = await fetch( + "http://localhost:8080/api/auth/register", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name, + username, + email, + password, + }), + }, + ); if (response.ok) { - navigate('/profile'); + navigate("/profile"); } else if (response.status === 409) { const errorText = await response.text(); - if (errorText === 'username exists') { + if (errorText === "username exists") { setUsernameError(errorText); - } else if (errorText === 'email exists') { + } else if (errorText === "email exists") { setEmailError(errorText); } } else { - throw new Error('Registration failed'); + throw new Error("Registration failed"); } } catch (error) { console.error(error); diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp index 9b127dbb..d8c48176 100644 --- a/auth/src/api/AuthRequestHandlerFactory.cpp +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -18,112 +18,132 @@ using Poco::Net::HTTPServerResponse; class BaseCORSHandler : public Poco::Net::HTTPRequestHandler { protected: - void addCORSHeaders(Poco::Net::HTTPServerResponse& response) { - response.set("Access-Control-Allow-Origin", "http://localhost:5173"); - response.set("Access-Control-Allow-Methods", "POST, OPTIONS"); - response.set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With"); - response.set("Access-Control-Max-Age", "86400"); - response.set("Vary", "Origin"); - } + void addCORSHeaders(Poco::Net::HTTPServerResponse& response) { + response.set("Access-Control-Allow-Origin", "http://localhost:5173"); + response.set("Access-Control-Allow-Methods", "POST, OPTIONS"); + response.set( + "Access-Control-Allow-Headers", + "Content-Type, Authorization, X-Requested-With" + ); + response.set("Access-Control-Max-Age", "86400"); + response.set("Vary", "Origin"); + } }; class CORSHandler : public BaseCORSHandler { public: - void handleRequest(Poco::Net::HTTPServerRequest& request, - Poco::Net::HTTPServerResponse& response) override { - addCORSHeaders(response); - if (request.getMethod() == "OPTIONS") { - response.setStatus(Poco::Net::HTTPResponse::HTTP_OK); - response.send(); - } else { - response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED); - response.send(); - } + void handleRequest( + Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response + ) override { + addCORSHeaders(response); + if (request.getMethod() == "OPTIONS") { + response.setStatus(Poco::Net::HTTPResponse::HTTP_OK); + response.send(); + } else { + response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED); + response.send(); } + } }; class CORSHandlerWrapper : public BaseCORSHandler { public: - CORSHandlerWrapper(Poco::Net::HTTPRequestHandler* handler) - : m_handler(handler) {} + CORSHandlerWrapper(Poco::Net::HTTPRequestHandler* handler) + : m_handler(handler) {} - ~CORSHandlerWrapper() override { - delete m_handler; - } + ~CORSHandlerWrapper() override { delete m_handler; } - void handleRequest(Poco::Net::HTTPServerRequest& request, - Poco::Net::HTTPServerResponse& response) override { - addCORSHeaders(response); - m_handler->handleRequest(request, response); - } + void handleRequest( + Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response + ) override { + addCORSHeaders(response); + m_handler->handleRequest(request, response); + } private: - Poco::Net::HTTPRequestHandler* m_handler; + Poco::Net::HTTPRequestHandler* m_handler; }; class CORSRegistrationHandler : public BaseCORSHandler { public: - explicit CORSRegistrationHandler(DBConnector& db) : m_handler(db) {} + explicit CORSRegistrationHandler(DBConnector& db) + : m_handler(db) {} + + void handleRequest( + Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response + ) override { + addCORSHeaders(response); + m_handler.handleRequest(request, response); + } - void handleRequest(Poco::Net::HTTPServerRequest& request, - Poco::Net::HTTPServerResponse& response) override { - addCORSHeaders(response); - m_handler.handleRequest(request, response); - } private: - RegistrationHandler m_handler; + RegistrationHandler m_handler; }; class CORSLoginHandler : public BaseCORSHandler { public: - CORSLoginHandler(DBConnector& db, TokenManager& tokenManager) - : m_handler(db, tokenManager) {} + CORSLoginHandler(DBConnector& db, TokenManager& tokenManager) + : m_handler(db, tokenManager) {} + + void handleRequest( + Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response + ) override { + addCORSHeaders(response); + m_handler.handleRequest(request, response); + } - void handleRequest(Poco::Net::HTTPServerRequest& request, - Poco::Net::HTTPServerResponse& response) override { - addCORSHeaders(response); - m_handler.handleRequest(request, response); - } private: - LoginHandler m_handler; + LoginHandler m_handler; }; class CORSVerificationHandler : public BaseCORSHandler { public: - explicit CORSVerificationHandler(TokenManager& tokenManager) - : m_handler(tokenManager) {} + explicit CORSVerificationHandler(TokenManager& tokenManager) + : m_handler(tokenManager) {} + + void handleRequest( + Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response + ) override { + addCORSHeaders(response); + m_handler.handleRequest(request, response); + } - void handleRequest(Poco::Net::HTTPServerRequest& request, - Poco::Net::HTTPServerResponse& response) override { - addCORSHeaders(response); - m_handler.handleRequest(request, response); - } private: - VerificationHandler m_handler; + VerificationHandler m_handler; }; class CORSRefreshHandler : public BaseCORSHandler { public: - explicit CORSRefreshHandler(TokenManager& tokenManager) - : m_handler(tokenManager) {} + explicit CORSRefreshHandler(TokenManager& tokenManager) + : m_handler(tokenManager) {} + + void handleRequest( + Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response + ) override { + addCORSHeaders(response); + m_handler.handleRequest(request, response); + } - void handleRequest(Poco::Net::HTTPServerRequest& request, - Poco::Net::HTTPServerResponse& response) override { - addCORSHeaders(response); - m_handler.handleRequest(request, response); - } private: - RefreshHandler m_handler; + RefreshHandler m_handler; }; class CORSNotFoundHandler : public BaseCORSHandler { public: - void handleRequest(Poco::Net::HTTPServerRequest& request, - Poco::Net::HTTPServerResponse& response) override { - addCORSHeaders(response); - response.setStatus(Poco::Net::HTTPResponse::HTTP_NOT_FOUND); - response.send(); - } + void handleRequest( + Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse& response + ) override { + addCORSHeaders(response); + response.setStatus(Poco::Net::HTTPResponse::HTTP_NOT_FOUND); + response.send(); + } }; AuthRequestHandlerFactory::AuthRequestHandlerFactory( @@ -133,22 +153,23 @@ AuthRequestHandlerFactory::AuthRequestHandlerFactory( , m_tokenManager(tokenManager) {} HTTPRequestHandler* -AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request) { - if (request.getMethod() == "OPTIONS") { - return new CORSHandler(); - } - - if (request.getMethod() == "POST") { - if (request.getURI() == "/api/auth/register") { - return new CORSRegistrationHandler(m_db); - } else if (request.getURI() == "/api/auth/login") { - return new CORSLoginHandler(m_db, m_tokenManager); - } else if (request.getURI() == "/api/auth/verify") { - return new CORSVerificationHandler(m_tokenManager); - } else if (request.getURI() == "/api/auth/refresh") { - return new CORSRefreshHandler(m_tokenManager); - } +AuthRequestHandlerFactory::createRequestHandler(HTTPServerRequest const& request +) { + if (request.getMethod() == "OPTIONS") { + return new CORSHandler(); + } + + if (request.getMethod() == "POST") { + if (request.getURI() == "/api/auth/register") { + return new CORSRegistrationHandler(m_db); + } else if (request.getURI() == "/api/auth/login") { + return new CORSLoginHandler(m_db, m_tokenManager); + } else if (request.getURI() == "/api/auth/verify") { + return new CORSVerificationHandler(m_tokenManager); + } else if (request.getURI() == "/api/auth/refresh") { + return new CORSRefreshHandler(m_tokenManager); } + } - return new CORSNotFoundHandler(); -} \ No newline at end of file + return new CORSNotFoundHandler(); +} From 4187ec0e3565a0f113786f867ba646f7bfbb19fc Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 00:12:40 +0300 Subject: [PATCH 107/152] customBlockUtils tests introduced --- .../codeComponents/PaneContextMenu.jsx | 2 +- .../unit tests/customBlockUtils.unit.test.js | 111 ++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 UI/src/components/utils/__tests__/unit tests/customBlockUtils.unit.test.js diff --git a/UI/src/components/codeComponents/PaneContextMenu.jsx b/UI/src/components/codeComponents/PaneContextMenu.jsx index 1428bc4b..82a2f0d6 100644 --- a/UI/src/components/codeComponents/PaneContextMenu.jsx +++ b/UI/src/components/codeComponents/PaneContextMenu.jsx @@ -72,7 +72,7 @@ export default function PaneContextMenu({ onClose(); }} > - Add custom circuit + Create custom circuit
); diff --git a/UI/src/components/utils/__tests__/unit tests/customBlockUtils.unit.test.js b/UI/src/components/utils/__tests__/unit tests/customBlockUtils.unit.test.js new file mode 100644 index 00000000..ea8e23cf --- /dev/null +++ b/UI/src/components/utils/__tests__/unit tests/customBlockUtils.unit.test.js @@ -0,0 +1,111 @@ +import { + generateCustomBlockId, + createCustomBlock, + saveCustomBlock, + loadCustomBlocks, + deleteCustomBlock, + findCustomBlockById, +} from '../../customBlockUtils.js'; + +beforeEach(() => { + localStorage.clear(); + jest.clearAllMocks(); +}); + +describe('generateCustomBlockId', () => { + it('should generate a unique id with correct format', () => { + const id = generateCustomBlockId(); + expect(id).toMatch(/^custom_[a-z0-9]+_[a-z0-9]{8}$/); + }); + + it('should generate different IDs on consecutive calls', () => { + const id1 = generateCustomBlockId(); + const id2 = generateCustomBlockId(); + expect(id1).not.toEqual(id2); + }); +}); + +describe('createCustomBlock', () => { + const nodes = [ + { id: '1', type: 'inputNodeSwitch', name: 'Switch A' }, + { id: '2', type: 'inputNodeButton' }, + { id: '3', type: 'outputNodeLed', name: 'LED' }, + { id: '4', type: 'logicNode' }, + ]; + const edges = [{ id: 'e1', source: '1', target: '3' }]; + + it('should create a custom block with inputs and outputs', () => { + const block = createCustomBlock(nodes, edges, 'My Block'); + + expect(block).toHaveProperty('id'); + expect(block).toHaveProperty('name', 'My Block'); + expect(block.inputs).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: '1', name: 'Switch A' }), + expect.objectContaining({ id: '2' }), + ]) + ); + expect(block.outputs).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: '3', name: 'LED' }), + ]) + ); + expect(block.originalSchema.nodes).toBe(nodes); + expect(block.originalSchema.edges).toBe(edges); + }); + + it('should throw error if nodes or edges are not arrays', () => { + expect(() => createCustomBlock(null, [], 'Block')).toThrow(); + expect(() => createCustomBlock([], null, 'Block')).toThrow(); + }); +}); + +describe('saveCustomBlock & loadCustomBlocks', () => { + it('should save and load a block correctly', () => { + const block = { id: 'test-id', name: 'Test', inputs: [], outputs: [], originalSchema: {} }; + saveCustomBlock(block); + + const loaded = loadCustomBlocks(); + expect(loaded).toHaveLength(1); + expect(loaded[0]).toEqual(block); + }); + + it('should handle malformed JSON gracefully', () => { + localStorage.setItem('customBlocks', 'invalid_json'); + const loaded = loadCustomBlocks(); + expect(loaded).toEqual([]); + }); +}); + +describe('deleteCustomBlock', () => { + it('should delete the specified block', () => { + const block = { id: 'to-delete', name: 'To Delete', inputs: [], outputs: [], originalSchema: {} }; + saveCustomBlock(block); + expect(loadCustomBlocks()).toHaveLength(1); + + const result = deleteCustomBlock('to-delete'); + expect(result).toBe(true); + expect(loadCustomBlocks()).toHaveLength(0); + }); + + it('should return false if deletion fails', () => { + localStorage.setItem('customBlocks', 'bad_json'); + const result = deleteCustomBlock('any-id'); + expect(result).toBe(false); + }); +}); + +describe('findCustomBlockById', () => { + it('should return the correct block by ID', () => { + const block = { id: 'block123', name: 'FindMe', inputs: [], outputs: [], originalSchema: {} }; + saveCustomBlock(block); + + const found = findCustomBlockById('block123'); + expect(found).toEqual(block); + }); + + it('should return undefined for nonexistent ID', () => { + const found = findCustomBlockById('missing'); + expect(found).toBeUndefined(); + }); +}); From f59170ae3c73ab8d9464881288094170f54a312a Mon Sep 17 00:00:00 2001 From: arsenez Date: Sun, 20 Jul 2025 00:14:47 +0300 Subject: [PATCH 108/152] Fix profile deployment --- docker-compose.yml | 6 ------ nginx.conf | 6 ++++++ postgres-init/projects.sql | 11 +++++++++++ postgres-init/users.sql | 12 ------------ 4 files changed, 17 insertions(+), 18 deletions(-) create mode 100644 postgres-init/projects.sql diff --git a/docker-compose.yml b/docker-compose.yml index c4b877a0..139572e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,6 @@ services: postgres: image: postgres:17-alpine - ports: - - "5432:5432" environment: POSTGRES_DB: "vcd" POSTGRES_USER: "vcd" @@ -20,8 +18,6 @@ services: auth: build: ./auth - ports: - - "8080:8080" environment: POSTGRES_DB: "vcd" POSTGRES_USER: "vcd" @@ -34,8 +30,6 @@ services: profile: build: ./backend - ports: - - "8081:8080" environment: AUTH_SERVICE_URL: "http://auth:8080" POSTGRES_DB: "vcd" diff --git a/nginx.conf b/nginx.conf index 8415b0f9..9476b85b 100644 --- a/nginx.conf +++ b/nginx.conf @@ -22,6 +22,12 @@ server { proxy_set_header X-Real-IP $remote_addr; } + location /api/profile { + proxy_pass http://profile:8080; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + } + location / { root /usr/share/nginx/html; try_files $uri /index.html; diff --git a/postgres-init/projects.sql b/postgres-init/projects.sql new file mode 100644 index 00000000..a09f5dd3 --- /dev/null +++ b/postgres-init/projects.sql @@ -0,0 +1,11 @@ +CREATE TABLE public.projects ( + pid SERIAL PRIMARY KEY, + owner_id INTEGER NOT NULL REFERENCES users(id), + name VARCHAR(100) NOT NULL, + circuit JSONB, + custom_nodes JSONB, + verilog TEXT, + created_at TIMESTAMPTZ DEFAULT now() NOT NULL +); + +ALTER TABLE public.projects OWNER TO vcd; diff --git a/postgres-init/users.sql b/postgres-init/users.sql index 9eaf407e..ff256c90 100644 --- a/postgres-init/users.sql +++ b/postgres-init/users.sql @@ -33,15 +33,3 @@ ALTER TABLE ONLY public.users ALTER TABLE ONLY public.users ADD CONSTRAINT users_username_key UNIQUE (username); - -CREATE TABLE public.projects ( - pid SERIAL PRIMARY KEY, - owner_id INTEGER NOT NULL REFERENCES users(id), - name VARCHAR(100) NOT NULL, - circuit JSONB, - custom_nodes JSONB, - verilog TEXT, - created_at TIMESTAMPTZ DEFAULT now() NOT NULL -); - -ALTER TABLE public.projects OWNER TO vcd; \ No newline at end of file From 980464dea82ce918aa62c54c5e878d55afb07223 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 00:21:48 +0300 Subject: [PATCH 109/152] button name changed --- UI/src/components/codeComponents/PaneContextMenu.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/src/components/codeComponents/PaneContextMenu.jsx b/UI/src/components/codeComponents/PaneContextMenu.jsx index 82a2f0d6..220534b0 100644 --- a/UI/src/components/codeComponents/PaneContextMenu.jsx +++ b/UI/src/components/codeComponents/PaneContextMenu.jsx @@ -72,7 +72,7 @@ export default function PaneContextMenu({ onClose(); }} > - Create custom circuit + Create custom node
); From 59d22d80ee116acadb4854ce017ab2467a5acc9e Mon Sep 17 00:00:00 2001 From: witch2256 Date: Sun, 20 Jul 2025 00:38:49 +0300 Subject: [PATCH 110/152] =?UTF-8?q?=D1=88=D1=91=D0=BB=20=D0=BC=D0=B5=D0=B4?= =?UTF-8?q?=D0=B2=D0=B5=D0=B4=D1=8C=20=D0=BF=D0=BE=20=D0=BB=D0=B5=D1=81?= =?UTF-8?q?=D1=83,=20=D0=B2=D0=B8=D0=B4=D0=B8=D1=82=20-=20=D0=BC=D0=B0?= =?UTF-8?q?=D1=88=D0=B8=D0=BD=D0=B0=20=D0=B3=D0=BE=D1=80=D0=B8=D1=82.=20?= =?UTF-8?q?=D1=81=D0=B5=D0=BB=20=D0=B2=20=D0=BD=D0=B5=D1=91=20=D0=B8=20?= =?UTF-8?q?=D1=81=D0=B3=D0=BE=D1=80=D0=B5=D0=BB=20=F0=9F=92=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UI/src/components/pages/auth.jsx | 2 +- UI/src/components/pages/register.jsx | 2 +- auth/src/api/AuthRequestHandlerFactory.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index b2f6ef36..e61fbd18 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -66,7 +66,7 @@ const Auth = () => { return; } try { - const response = await fetch("http://localhost:8080/api/auth/login", { + const response = await fetch("http://auth:8080/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ login, password }), diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index 811beafb..cdc768f7 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -166,7 +166,7 @@ const Auth = () => { if (!hasErrors) { try { const response = await fetch( - "http://localhost:8080/api/auth/register", + "http://auth:8080/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/auth/src/api/AuthRequestHandlerFactory.cpp b/auth/src/api/AuthRequestHandlerFactory.cpp index d8c48176..a31ef8b3 100644 --- a/auth/src/api/AuthRequestHandlerFactory.cpp +++ b/auth/src/api/AuthRequestHandlerFactory.cpp @@ -19,7 +19,7 @@ using Poco::Net::HTTPServerResponse; class BaseCORSHandler : public Poco::Net::HTTPRequestHandler { protected: void addCORSHeaders(Poco::Net::HTTPServerResponse& response) { - response.set("Access-Control-Allow-Origin", "http://localhost:5173"); + response.set("Access-Control-Allow-Origin", "/"); response.set("Access-Control-Allow-Methods", "POST, OPTIONS"); response.set( "Access-Control-Allow-Headers", From bd2fe5fa6f397138eedc29bac54e1e6a66d373f1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:39:39 +0000 Subject: [PATCH 111/152] Automated formatting --- UI/src/components/pages/register.jsx | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index cdc768f7..fbb07fcc 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -165,19 +165,16 @@ const Auth = () => { if (!hasErrors) { try { - const response = await fetch( - "http://auth:8080/api/auth/register", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name, - username, - email, - password, - }), - }, - ); + const response = await fetch("http://auth:8080/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name, + username, + email, + password, + }), + }); if (response.ok) { navigate("/profile"); } else if (response.status === 409) { From 4df1ccff9de6630283f6a720d0cf2bf4f58352c2 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 10:11:45 +0300 Subject: [PATCH 112/152] creation of custom node require at least one input node and one output node. all inputs should have a name --- UI/src/components/pages/mainPage.jsx | 6 ++-- UI/src/components/utils/customBlockUtils.js | 34 +++++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 1d54efdb..f5a214b3 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -776,9 +776,9 @@ export default function Main() { setModalOpen(false)} - nodes={nodes} - edges={edges} - onCreateFromFile={handleCreateFromFile} + nodes={getSelectedElements().nodes} + edges={getSelectedElements().edges} + // onCreateFromFile={handleCreateFromFile} onCreateFromCurrent={handleCreateFromCurrent} /> diff --git a/UI/src/components/utils/customBlockUtils.js b/UI/src/components/utils/customBlockUtils.js index aa48e8a4..ebf2c150 100644 --- a/UI/src/components/utils/customBlockUtils.js +++ b/UI/src/components/utils/customBlockUtils.js @@ -9,25 +9,35 @@ export const createCustomBlock = (nodes, edges, blockName) => { if (!Array.isArray(nodes)) throw new Error("Invalid nodes: must be an array"); if (!Array.isArray(edges)) throw new Error("Invalid edges: must be an array"); - const inputs = nodes.filter(node => + const inputNodes = nodes.filter(node => node.type === "inputNodeSwitch" || node.type === "inputNodeButton" - ).map(node => ({ - id: node.id, - name: node.name || `input_${Math.floor(Math.random() * 10000)}`, - })); + ); - const outputs = nodes.filter(node => + const outputNodes = nodes.filter(node => node.type === "outputNodeLed" - ).map(node => ({ - id: node.id, - name: node.name || `output_${Math.floor(Math.random() * 10000)}`, - })); + ); + + if (inputNodes.length === 0 || outputNodes.length === 0) { + throw new Error("Custom block must have at least one input and one output pin"); + } + + inputNodes.forEach(node => { + if (!node.name) { + throw new Error(`Input \"${node.type.replace("inputNode", "")}\" must have a name`); + } + }); + + outputNodes.forEach(node => { + if (!node.name) { + throw new Error(`Output \"${node.type.replace("outputNode", "")}\" must have a name`); + } + }); return { id: generateCustomBlockId(), name: blockName, - inputs, - outputs, + inputNodes, + outputNodes, originalSchema: { nodes, edges }, }; }; From 9a7dd51dc118693b5a75788b91816acdeebb499f Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 11:17:57 +0300 Subject: [PATCH 113/152] new toolbar button added --- UI/assets/toolbar-icons.jsx | 37 ++++++++++++++++++++ UI/src/CSS/toolbar.css | 31 ++++++++++++++++ UI/src/components/pages/mainPage.jsx | 4 ++- UI/src/components/pages/mainPage/toolbar.jsx | 28 ++++++++++++++- 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/UI/assets/toolbar-icons.jsx b/UI/assets/toolbar-icons.jsx index 5d44e274..a9321902 100644 --- a/UI/assets/toolbar-icons.jsx +++ b/UI/assets/toolbar-icons.jsx @@ -119,3 +119,40 @@ export const IconToolbarText = ({ SVGClassName }) => ( /> ); + +export const IconToolbarCustomBlock = ({ SVGClassName }) => ( + + + + + + + +) \ No newline at end of file diff --git a/UI/src/CSS/toolbar.css b/UI/src/CSS/toolbar.css index c23f00a0..17214261 100644 --- a/UI/src/CSS/toolbar.css +++ b/UI/src/CSS/toolbar.css @@ -158,3 +158,34 @@ .toolbar.download { right: 29.73rem; } + +.toolbar-button-group { + position: relative; +} + +.toolbar-dropdown { + position: absolute; + top: 100%; + left: 0; + z-index: 999; + background-color: var(--menu-lighter); + border: 1px solid var(--main-5); + border-radius: 6px; + padding: 4px 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + min-width: 160px; +} + +.dropdown-item { + padding: 6px 12px; + background: none; + border: none; + width: 100%; + text-align: left; + cursor: pointer; +} + +.dropdown-item:hover { + background-color: var(--select-2); +} + diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index f5a214b3..cdeff532 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -67,6 +67,7 @@ import { handleTabSwitch as handleTabSwitchUtil } from "../utils/handleTabSwitch import { getEditableNode } from "../utils/getEditableNode.js"; import { handleNameChange } from "../utils/handleNameChange.js"; import CreateCustomBlockModal from "./mainPage/CreateCustomBlockModal.jsx"; +import { createCustomBlock, saveCustomBlock } from "../utils/customBlockUtils.js"; export const SimulateStateContext = createContext({ simulateState: "idle", @@ -778,7 +779,7 @@ export default function Main() { onClose={() => setModalOpen(false)} nodes={getSelectedElements().nodes} edges={getSelectedElements().edges} - // onCreateFromFile={handleCreateFromFile} + onCreateFromFile={handleCreateFromFile} onCreateFromCurrent={handleCreateFromCurrent} /> @@ -819,6 +820,7 @@ export default function Main() { loadCircuit={loadCircuit} fileInputRef={fileInputRef} handleOpenClick={handleOpenClick} + onCreateCustomBlock={() => setModalOpen(true)} setMenu={setMenu} onSimulateClick={() => handleSimulateClick({ diff --git a/UI/src/components/pages/mainPage/toolbar.jsx b/UI/src/components/pages/mainPage/toolbar.jsx index 3f0efd32..8ca92511 100644 --- a/UI/src/components/pages/mainPage/toolbar.jsx +++ b/UI/src/components/pages/mainPage/toolbar.jsx @@ -17,6 +17,7 @@ import { IconToolbarStepWire, IconToolbarStraightWire, IconToolbarText, + IconToolbarCustomBlock, } from "../../../../assets/toolbar-icons.jsx"; export default function Toolbar({ @@ -31,6 +32,7 @@ export default function Toolbar({ loadCircuit, fileInputRef, handleOpenClick, + onCreateCustomBlock, undo, redo, canUndo, @@ -210,7 +212,10 @@ export default function Toolbar({ onClick={handleOpenClick} title={"Upload"} > - + + +
+ + +
); From 55b5dc789bb135f4c31b55283c32ff7f344110d9 Mon Sep 17 00:00:00 2001 From: arsenez Date: Sun, 20 Jul 2025 11:59:08 +0300 Subject: [PATCH 114/152] Fix postgres initdb order --- postgres-init/{users.sql => 1-users.sql} | 0 postgres-init/{projects.sql => 2-projects.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename postgres-init/{users.sql => 1-users.sql} (100%) rename postgres-init/{projects.sql => 2-projects.sql} (100%) diff --git a/postgres-init/users.sql b/postgres-init/1-users.sql similarity index 100% rename from postgres-init/users.sql rename to postgres-init/1-users.sql diff --git a/postgres-init/projects.sql b/postgres-init/2-projects.sql similarity index 100% rename from postgres-init/projects.sql rename to postgres-init/2-projects.sql From b52ec19a0dd6322b17a5f175fedbbf8b40ce291f Mon Sep 17 00:00:00 2001 From: Arseny <34488301+arsenez2006@users.noreply.github.com> Date: Sun, 20 Jul 2025 12:15:56 +0300 Subject: [PATCH 115/152] Fix runner url --- UI/src/components/pages/mainPage/runnerHandler.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/src/components/pages/mainPage/runnerHandler.jsx b/UI/src/components/pages/mainPage/runnerHandler.jsx index 7854bf73..744aff88 100644 --- a/UI/src/components/pages/mainPage/runnerHandler.jsx +++ b/UI/src/components/pages/mainPage/runnerHandler.jsx @@ -62,7 +62,7 @@ export const handleSimulateClick = ({ // Initialize socket connection if (!socketRef.current) { - socketRef.current = io("http://localhost:80", { + socketRef.current = io("/", { transports: ["websocket"], path: "/socket.io", }); From 4999c076086b128e3325afa138753138080f5260 Mon Sep 17 00:00:00 2001 From: Arseny <34488301+arsenez2006@users.noreply.github.com> Date: Sun, 20 Jul 2025 12:22:37 +0300 Subject: [PATCH 116/152] Fix postgres health-check timer --- docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 139572e7..c2044f16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,9 +9,9 @@ services: - ./postgres-init:/docker-entrypoint-initdb.d healthcheck: test: [ "CMD-SHELL", "pg_isready -U vcd -d vcd" ] - interval: 5s - timeout: 5s - retries: 10 + interval: 30s + timeout: 10s + retries: 5 runner: build: ./RunnerNode @@ -48,4 +48,4 @@ services: depends_on: - runner - auth - - profile \ No newline at end of file + - profile From 7680c2597a2b0ea364736a12f3ff5286d0148e51 Mon Sep 17 00:00:00 2001 From: witch2256 Date: Sun, 20 Jul 2025 13:45:58 +0300 Subject: [PATCH 117/152] fixed issue with pages and renamed pages domains --- UI/src/App.jsx | 4 ++++ UI/src/CSS/auth.css | 1 + UI/src/components/pages/auth.jsx | 2 +- UI/src/components/pages/mainPage.jsx | 2 +- UI/src/components/pages/register.jsx | 4 ++-- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/UI/src/App.jsx b/UI/src/App.jsx index 7062bafe..36926ce0 100644 --- a/UI/src/App.jsx +++ b/UI/src/App.jsx @@ -1,6 +1,8 @@ import Main from "./components/pages/mainPage.jsx"; import Profile from "./components/pages/profile.jsx"; import HelloPage from "./components/pages/hello-page.jsx"; +import Auth from "./components/pages/auth.jsx"; +import Reg from "./components/pages/register.jsx" import "@xyflow/react/dist/style.css"; @@ -40,6 +42,8 @@ function App() { } /> } /> } /> + } /> + } />
diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index 3c6be920..0e892568 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -117,6 +117,7 @@ font-family: Montserrat, serif; margin-top: 4.5rem; color: var(--main-0); + text-decoration-color: var(--main-0); } .register-link { diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index e61fbd18..66885e7e 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -140,7 +140,7 @@ const Auth = () => { )}
Have no account?
- + Register
diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 10365778..6b18fc31 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -744,7 +744,7 @@ export default function Main() { diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index fbb07fcc..2b08a2c1 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -6,7 +6,7 @@ import { useNavigate } from "react-router-dom"; const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; -const Auth = () => { +const Reg = () => { const navigate = useNavigate(); // Состояния полей формы @@ -307,4 +307,4 @@ const Auth = () => { ); }; -export default Auth; +export default Reg; From 81b6f9add756b437115731b8807f10733e37f43e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 10:46:32 +0000 Subject: [PATCH 118/152] Automated formatting --- UI/src/App.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/src/App.jsx b/UI/src/App.jsx index 36926ce0..4cbf0a6d 100644 --- a/UI/src/App.jsx +++ b/UI/src/App.jsx @@ -2,7 +2,7 @@ import Main from "./components/pages/mainPage.jsx"; import Profile from "./components/pages/profile.jsx"; import HelloPage from "./components/pages/hello-page.jsx"; import Auth from "./components/pages/auth.jsx"; -import Reg from "./components/pages/register.jsx" +import Reg from "./components/pages/register.jsx"; import "@xyflow/react/dist/style.css"; From 62f80c795ca9e0a253f701886eb66b36b86594e6 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 14:20:48 +0300 Subject: [PATCH 119/152] custom blocks can be created with toolbar button from the file --- UI/src/CSS/customBlock.css | 96 ++++++++++--------- UI/src/components/pages/mainPage.jsx | 61 ++++++++---- .../pages/mainPage/createCustomBlockModal.jsx | 22 ++--- UI/src/components/pages/mainPage/toolbar.jsx | 18 ++-- UI/src/components/utils/hotkeyHandler.js | 10 +- 5 files changed, 122 insertions(+), 85 deletions(-) diff --git a/UI/src/CSS/customBlock.css b/UI/src/CSS/customBlock.css index b3c8d7c5..937a222f 100644 --- a/UI/src/CSS/customBlock.css +++ b/UI/src/CSS/customBlock.css @@ -1,18 +1,3 @@ -.create-button { - height: 2rem; - padding: 0 15px; - background-color: var(--status-success-1); - color: var(--main-0); - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 16px; - transition: background-color 0.3s; - position: fixed; - left: 5.64rem; - top: calc(0.5rem + 5vh); -} - .custom-block-modal { position: fixed; top: 0; @@ -26,8 +11,8 @@ } .modal-content { - border: 1px solid var(--main-5); - background: var(--main-2); + border: var(--main-4) solid 0.1rem; + background: var(--menu-lighter); padding: 25px; color: var(--main-0); border-radius: 8px; @@ -37,7 +22,6 @@ & > h3 { font-size: 1.2rem; - margin-bottom: 40px; } } @@ -51,50 +35,44 @@ cursor: pointer; } -.close-custom-circuit-cross { - width: 1rem; - height: 1rem; - color: var(--main-0); +.close-button-custom-circuit:hover { + top: 5px; + right: 5px; + padding: 5px 10px; + border-radius: 50%; + background-color: var(--status-error-1); } -.creation-options { - display: flex; - flex-direction: column; - gap: 10px; +.close-button-custom-circuit:active { + top: 5px; + right: 5px; + padding: 5px 10px; + border-radius: 50%; + background-color: var(--status-error-2); } -.option-button { +.close-custom-circuit-cross { + width: 1rem; + height: 1rem; color: var(--main-0); - padding: 12px 20px; - background-color: var(--main-3); - border: 1px solid var(--main-4); - border-radius: 4px; - cursor: pointer; - font-size: 16px; - text-align: center; - transition: background-color 0.15s; -} - -.option-button:hover { - background-color: var(--main-4); } -.current-circuit-option { +.creation-options { display: flex; flex-direction: column; gap: 10px; } .name-input { - margin-top: 10px; display: flex; flex-direction: column; } .name-input input { + margin-bottom: 20px; padding: 10px; - border: 1px solid var(--main-4); - background: var(--main-2); + border: var(--main-4) solid 0.1rem; + background: var(--menu-lighter); border-radius: 4px; font-size: 16px; color: var(--main-0); @@ -106,6 +84,38 @@ box-shadow: 0 0 0 2px var(--select-1); } +.create-button { + margin-bottom: 20px; + color: var(--main-0); + padding: 12px 20px; + background-color: var(--menu-lighter); + border: var(--main-4) solid 0.1rem; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + text-align: center; + transition: background-color 0.15s; + user-select: none; +} + +.create-button:hover { + transform: translateY(-3px); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); +} + +.create-button:active { + transform: translateY(0px); + transition: 0.05s ease-out; + background-color: var(--select-2); + box-shadow: none; +} + +.current-circuit-option { + display: flex; + flex-direction: column; + gap: 10px; +} + .error-message { color: #f44336; font-size: 14px; diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index cdeff532..19d35a50 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -135,11 +135,43 @@ export default function Main() { const socketRef = useRef(null); - const fileInputRef = useRef(null); - const ignoreChangesRef = useRef(false); const [modalOpen, setModalOpen] = useState(false); + const [mode, setMode] = useState("fromSelected"); + const [nodesCustom, setNodesCustom] = useNodesState([]); + const [edgesCustom, setEdgesCustom] = useEdgesState([]); + + const uploadRef = useRef(null); + const extractRef = useRef(null); + + const handleUploadClick = () => { + uploadRef.current?.click(); + }; + + const handleExtractClick = () => { + extractRef.current?.click(); + }; + + const extractFromFile = useCallback( + (event) => { + loadCircuitUtil(event, setNodesCustom, setEdgesCustom); + setModalOpen(true); + setMode("fromFile"); + }, + [setNodesCustom, setEdgesCustom], + ); + + const onCreateCustom = useCallback( + (event) => { + const selectedElements = getSelectedElements(); + setNodesCustom(selectedElements.nodes); + setNodesCustom(selectedElements.edges); + setModalOpen(true); + setMode("fromFile"); + }, + [setNodesCustom, setEdgesCustom], + ); const handleCreateFromCurrent = (customBlock) => { // Handle custom block creation @@ -158,12 +190,6 @@ export default function Main() { const onNameChange = (e) => handleNameChange(e, editableNode, setNodes); - const handleOpenClick = () => { - if (fileInputRef.current) { - fileInputRef.current.click(); - } - }; - // Create history updater const historyUpdater = useMemo(() => createHistoryUpdater(), []); @@ -545,7 +571,8 @@ export default function Main() { setPanOnDrag, setActiveWire, socketRef, - handleOpenClick, + handleUploadClick, + handleExtractClick, undo, redo, }, @@ -567,7 +594,8 @@ export default function Main() { setPanOnDrag, setActiveWire, socketRef, - handleOpenClick, + handleUploadClick, + handleExtractClick, undo, redo, ], @@ -777,8 +805,8 @@ export default function Main() { setModalOpen(false)} - nodes={getSelectedElements().nodes} - edges={getSelectedElements().edges} + nodes={nodesCustom} + edges={edgesCustom} onCreateFromFile={handleCreateFromFile} onCreateFromCurrent={handleCreateFromCurrent} /> @@ -818,10 +846,11 @@ export default function Main() { setPanOnDrag={setPanOnDrag} saveCircuit={saveCircuit} loadCircuit={loadCircuit} - fileInputRef={fileInputRef} - handleOpenClick={handleOpenClick} - onCreateCustomBlock={() => setModalOpen(true)} - setMenu={setMenu} + extractFromFile={extractFromFile} + uploadRef={uploadRef} + handleUploadClick={handleUploadClick} + extractRef={extractRef} + handleExtractClick={handleExtractClick} onSimulateClick={() => handleSimulateClick({ simulateState, diff --git a/UI/src/components/pages/mainPage/createCustomBlockModal.jsx b/UI/src/components/pages/mainPage/createCustomBlockModal.jsx index 51b823a1..2c19754a 100644 --- a/UI/src/components/pages/mainPage/createCustomBlockModal.jsx +++ b/UI/src/components/pages/mainPage/createCustomBlockModal.jsx @@ -14,12 +14,11 @@ export default function CreateCustomBlockModal({ const [blockName, setBlockName] = useState(""); const [error, setError] = useState(""); - const handleCreateFromCurrent = () => { + const handleCreateCustomBlock = () => { if (!blockName.trim()) { showToastError("Please enter a custom block name."); return; } - try { const customBlock = createCustomBlock(nodes, edges, blockName.trim()); saveCustomBlock(customBlock); @@ -34,12 +33,7 @@ export default function CreateCustomBlockModal({ console.error("Error creating block:", err); setError(`Error: ${err.message}`); } - }; - - const handleCreateFromFile = () => { - onClose(); - if (onCreateFromFile) onCreateFromFile(); - }; + } if (!isOpen) return null; @@ -48,18 +42,11 @@ export default function CreateCustomBlockModal({

Create custom block

- -
-
+ {error &&

{error}

}
diff --git a/UI/src/components/pages/mainPage/toolbar.jsx b/UI/src/components/pages/mainPage/toolbar.jsx index 8ca92511..6986aafc 100644 --- a/UI/src/components/pages/mainPage/toolbar.jsx +++ b/UI/src/components/pages/mainPage/toolbar.jsx @@ -30,9 +30,11 @@ export default function Toolbar({ onSimulateClick, saveCircuit, loadCircuit, - fileInputRef, - handleOpenClick, - onCreateCustomBlock, + extractFromFile, + uploadRef, + handleUploadClick, + extractRef, + handleExtractClick, undo, redo, canUndo, @@ -209,7 +211,7 @@ export default function Toolbar({
diff --git a/UI/src/components/utils/hotkeyHandler.js b/UI/src/components/utils/hotkeyHandler.js index ba6ce826..5c8e21aa 100644 --- a/UI/src/components/utils/hotkeyHandler.js +++ b/UI/src/components/utils/hotkeyHandler.js @@ -24,7 +24,8 @@ export function hotkeyHandler(e, context) { socketRef, nodes, edges, - handleOpenClick, + handleUploadClick, + handleExtractClick, setActiveAction, setPanOnDrag, setActiveWire, @@ -103,7 +104,12 @@ export function hotkeyHandler(e, context) { case "o": case "щ": e.preventDefault(); - handleOpenClick?.(); + handleUploadClick?.(); + return; + case "b": + case "и": + e.preventDefault(); + handleExtractClick?.(); return; } } From 252c8dfbc14c2fae75a44455f53cd35461b57688 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sat, 19 Jul 2025 22:37:27 +0300 Subject: [PATCH 120/152] logo retrieved --- UI/assets/electronic-circuit.png | Bin 15728 -> 0 bytes UI/assets/logo.svg | 10 ++++++++++ UI/index.html | 1 + 3 files changed, 11 insertions(+) delete mode 100644 UI/assets/electronic-circuit.png create mode 100644 UI/assets/logo.svg diff --git a/UI/assets/electronic-circuit.png b/UI/assets/electronic-circuit.png deleted file mode 100644 index 2375a95f2cfa3d6c92d1ed3c32e599d78b9690a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15728 zcmd_RbyQSc-#>f+5kvt41cd<;B#E^Spn)Ydvedt~JhZ&OSTN+55Y{pEw~JY6_GT*C`+fqEvkJPz!>H z!B1j{{4Ds|_8B+?UuQgI6?MqLzX0+lZ^7T^Tpt;FKoHe8!iOlDn~DJ(yoQi7Kxn(z zAiOQzts!r3@4NQSjviK)uGV*5+-(z9?_Gx=7D(~oeI1{~m2uwy6RaivbbeyEva9Tl zVb#++#XcnE3E%014a+r&&oyVBiTH5Qj@Zx~S8V?Pvzw4ToXV-lqQRo7Odb_*DA`H&fK{>#8ZGRb7zRWa3f88~?&njmhKtOLVMKw~Aaw z^LRTp7A1MT(}O^ak`WEf8{Dn7|#4b+a%E{Ot_ z@(!+aF_jC}I(GRhx71U}bocZGxiOmR{jvimQ$+S`*gb8%C7VtsPx7F#hON3Tw!p@p zX%HlXM-L}Ru2irW;a8cPy|Stmj{K9oe?AAZCAG#kVsLe z9ZUovj~A7Fo2sPNoE6=JW=$78?#;i;en2=r`g18M2v?^Z?0nQyn)B}HmGta(Q+-1OL9fb1)3{B=;aJ2)2+|Nc`ZIAy--UOX1cL7L#c0)Xi(|8cAqe>; zBu8>6x*-+E0(#@xKXF5kbL2u_Vw5K!kgSfZ_BmjkI;Rb+=)ZI)_hM4&3s_h$Kub1ZK>;OyJp89D{wE! zzuq=TIi@)5#BcA21-+v=+}aZ~(}Lm9BS~YasO_tsS=HL)cj&x=>m0t^4D|bAc>PK) zAB;@yHKE&3tMUhPUYP!8|C$bkqcRPt!0fiIecuWIX#1cCjl*_&os$OP%mFSLfV^b8y z-%iqT3%MF)nkjlkeR1b=Thw}bw$+U`leWI*-s6{d^UW>b6iN^&M1M2o%D{2EZ#>Mf z5P3xeHjf0AZQY!A(6jqE*rsox@>*+s};;_E1$2 zziI6p)|1;HtMKaTLhW;Z+HS7|)JxWjUcra;oXkQnl3U=mGCuThEmA9PcsyV-&m00; z3@dQz!<-$vs>o0R#KQ9Y*A5LFm4#9fP0x79ay#0sB&jnf^@JQc^e z&{$9&Xl6;nL}5g?LO~Ij`R;vg`6KF~FZ`IDFhjZo)bo?&x1Mn*`qf%1&cn?=HZ z-xrH$Z7wM-EzHrJvT2FzWWpXlhMxsDi(eT@xXuTjG6-spDill^b1dcgl-O5cZHvdP zRw`{JX02U|yQT#yZ8GQ5p%w}$_T0!$UVd1{LV(L^o6a0{9Llm$!do(}>Ws2DYc&T5 zykG|BuVdp-nuy!-tqX^F^9i8HLy@eYi9<)iK0^BAE^ec$opw8=OwKjx|equgdSLj=rfQiBC9he=~m2UV4>LpyS@X-Y~ADj zYuQ#Z5$x)sD?SE8jcz$!f_xWQWr$=H6RyI)BVI<_C!#lHTJ%%3ZQSQQ+^w3-=N@)} zQ-yw*L_jR&i^q{VOD+Pij3+EBAo-b$?(>yiFj9+(4!>lx<mdv13_O!8ea%P zGNl<1L|=uYO(cRsVW6QBn691+oEUj2op78h=o{k9zpjlYqJT7(!4CKu{e@WsM$|$M zR@(&zQi$W3-c{uL7;p%JG79fNoZmqkp(8T!f#e)s;1vD`6Xp^!T^_WM5>1*aScyfP zNzL8Kd7RA(y2*XM#kd1ZQb-QXs|t!F{Z&MlzjzXHA=S=_c}C#6#xkfixy(qqrvI;F z*X_W%X(2Z>gxKr@1ESN12?Lh1$H=_FkwfzBBg7vzwEQfmh!m4?95Q{wjI$R&#|wF& zhziMr(7|s^VDWHpk>F~o9*UDdG!g~pk!+2Z12sl3^TwfwXu@oFoprbpzzhRTm_zq` z{nAg>8sYo?ZsL$wLr&d<5aGTXm_rkDIP7O@Bdm9Q`mi&`5~;ZoQX~oYuj&`AN>xk5tu3$8N8*- zY^RxJT%3C4WAE=vA;a z`p=+BEl5exszhE|>M-)qrt6kFtPR0PPgr!y{)&y){J$XHr|(7bgxj-#kVxX|I{Q+t z&UcQTgXk)#Yz7@(tR>KOxTBCSO^b0FHIOWiYAk;Cyh-dp7Q?S$0akd!6OnAuKj8@| z%?jt?I+I4+eGLBx+aY0IYZat5At=TFV!htJEDYjSMeFY391k^!u*9x23(VR0=$p3Lvx* zr$Q}UeNU^C7n_amoCLTg-#h7c^v>hy>5Q#{S34(zkPcBmg&?n1D{j+^Me#d~R~%SM z4&d`~^n42-s(^qFfid-W1vl#|IFy53p-0ePbisV$xDu3j>i*Z>MW`ZbN5H7>RNFJh zI}Rn_&@eoi9p1cdm1W4;d?_V{xJiBbB6TUrBs81HeTg=9OezQVqw}uIK^fAKBt!RA@EP)l(VF?@7$-0OW zzpS7)Ao~EfXr0N>dS`J;fM@N5`g_?)_VkR(7{gDiK^OJc49VsOCwrU{3uVmgF8_$8qq@hEE51sa^D+OzN5K)OW9z$0gl=*bs ziYnRtx6SKQKr{i<2coj%8_l3SsdkW?R1bA;LgH0o^tMkPav!>2EvniOiHV9&v=8Ue z&3F5{glv2^5z}*IG`(fVn;} z7rl=WfU)xuwk~P=K$Rg8USU`k+gSf$F4OQ)JQyzv1;qF88gG_9S&Y5u(^uKA=p3HS zn8=NBVM-@%{g&I2Up|KpM{|wbfA)3b1lA?Qc~5Vx03@eMn*Of6U5CF>Z$K9CToY_7 zO1^H5myEMiH?Z-S;HsFf=XJrJ7@sFGo(&F-#2TP0V)){-K9nWr@HY5XSA84Vd8Iht z_0bBhrMWSK8yuuPTt!XtulIs|Xo@_m@g>M^9RdTgK*lXBjEKm7rN;To@cBzREb1Hg zNaB7lT-%tUth=v0GAF_7ULj@Q_~Fr~cfwLSj-Wm}WyDfS)xFd=weyokG!P4!Uw{3M z)}g2$=OKgcVXH%c#i#feg4h{pqx+(w+yz;UnQ)A8r9|z0r##V0-tkVBnv;72JMns} z#>MJFJpI@!J^jBdvT%Rr(V zTtWXJqoGu#Q!!H7h{TU8 z)3`hww2PyvJ=)!s2_a?O8 zNAZ>C!RZi9?0?}f*e>Ve{|kOi9ISIFcKRrKHh6^Aur|t>;#O#gp%x-dh+?KU_}f*k zKXdaVnZ4)oIr)@TpBqNdvz>*+#3*E#NE1tJUaDiZf68L_+voz0ZseE%WaTKokJ!l^ zt>DKOirh{vT@>MmTELvzWSM#`%w6%wqpL@=ol_L15TP^e(yvGxV|Ra8&QEY7(~LPl z0pWvY4^NTfu zo4+HX?zss`lo@#9LyY>o3uu))iIHJqBzzaFohy5ao0?uyy zG@8)s+>xQwcMeVIMyF>mniScs(q?M~2l*S7t3*((0ulX->*g7Ea}Odq$N94}XiUDL z2Ys3oW;=9+cVr-qd~ot2;--<2Cl8ra zqxQ@EBA&c@CaA@R1{v%MOX(?U8q9aZztNs%F21Fk@^NWjMS%!kdoInEM%k;fElQbo+$6SL@m8Jd=&Ef_HMv zEmVYt=lwwQ!_ELC&H=5tDScO)mMw z4Q7x=CMdivem_a)830Lg^HHH)Gu|&iQV6{cAznuveB9C@JpGJ?sCp|Q(Xs(TzxXtv z?OlqXa@KdMZ5&1(UzBMO0E2*xmV{KMzwhIB3NcFzf2bY+)C=u#e^vGQka+4JxRC%8 ze|MP_lKXZ@%qJM|8hid^MyZ{1X!~d0UuE0DVID*%j{KIDd@SILzJFOH)|gMpGzyBu z(cj7d56ddxiYlSG=ck>e)v-Ou%L2jy5xa8U@hCR~lu6k63Og;=rdCoTo!<-vA10b2 zZ=~I)d{Q?MHrsF+s#(N+N5A?T3lAIVPb%;E zW@j*d=sjoWwST9ZQ{pJqO8REL&NuTszf+p-kVJIsP_bt)_qP18n{|WE^DE)bQf=6Q zb}^VaHXY+=yZAodFn^ociO;Tbve+SLM%|p*TzJ-WGTb`Suhu&ud38LO&cAvQ`y56? zOqhl3d2fxgj3ythbXCNoHtA3lOgWKUcdzZ=V|v{L0g5Z+<){IJMa={4kB87iIfx`5$N@2IDaD?b&c6JDtt)P=#H3P9^$CHhY>;W^eQ#rJ zTYp+j07k|^K?BXmMLSkO`-w=k!u^7KF5mk)Xk$9{WxH;KQ)3rV;wgPA<;8>FTsP3D zVfw!)XiX&g$iTLkZT;5wj5Qm2&0#PG$Rn|zr2K5QH zr^|CquniG)Nuu7_x#J!ki#fQx=Oam5J{AmHD^pZ8ihnGhrP(N&@h}as^t2Ir*DdINYc639X6Fr&bX^mS%kA zaBO`zd(69Vs7TlDi8{X8(bxGggPZ+Me^77ps%az@Q7Xiu{sL7iI@*zT@M_YgWPOw| zuS7e`e=f+3U5D1z_ZsR-fe~VoUzG1I^otq{>yqnh5!h3yfg@vz=5Cqa)#8_F^M`Iu zddBNI_dYhjbY0(BvTDkWyDDirr9l`|v*YR@|CJbKuJOYcE?bXu9TI!i%?sWx9|?_J z%pa88!pm#3&berls$>9yomjy8Y|FPQRkKZc)Z6^ZpQoIpy71tW*99UPMdm-6)l^NW zkh1{PiA#~NHmN@Ks3E3UqM2m>i-`*Re0^MaFJ4oylHYU_>@yu zd>W+X!r3vxi&f(KU2BIRhQ9}6=j|VUi6j5j`*fZdYhOly79qMkhn+L;Kxq8yycUx! zB>(QL?WgQoTS7b!wE1(H2c2LGKda$Nm?Zu0;!u#{CG3LOq{6M}2{!54Kka?q+2%#9 z7GVI5-`9jb(4E8qFu-EpwDy1^^ttmJZ3d2^>g>Q@0Nba6JM?Zs1>pEWhP5BM8G@q# z7?xHzC-)R3ID#hlKog{-Eq790#4k1Vf^%EwJZ8PP-Z3^knHz+v1BcZxmkLG3C6%pv z=t2Be)6UgPNiwL3W+RFuELcJ^_L|s2Ny9pRG1(cQxWgxy$9hyRX$dEH2W+ex-{a^9 z9o*C-%@bRtw7^m*vNL)DKl{NOv~1$?wp?fQF>Nn7|M|~uN zibBbt<|?Ni?{*}&FkT?A!YvI)FJlJIY6lOSdD?mbwhV1Ee+EFIR%-xT#V8+k=(7VTMhNI)I$*45y~1LvcM8DoMqLYsDvZ;H@uS* zT(-l|YitN`4x6oug3#Z{9x!hLAPL4M&22ZILz$?$639ZCOQm(tqyFsu@@&?q_hB2h zpsRY7g7ZEVmuz&9ng`h@a7!`I)ME0;c_-h1J=`fRymcCd_= zj3wq0M010pg=kr&2(wg5n{D;p$kD0`8&81z6TiI;s+n5WL2~nF_VZ*uYvnNFy=D$; zrDE|z0&9M>5kd@E>~lb5AO8m7SF4BB)%~jVc286ctpOALES3an`IrhZ%7UAd5m?P9 zjm-S~H2l-vNi0zWq@>ml;J3vY(kZ^j!)82d_1_msga1q*nQ$lGa^*)V3ZIEGzu&vi z@@PA2Ik6*(nD0zu9F^q276~^svh-;&6Ryz^9KdM0Uc7b-3^a6P*Cz%?F0TQyMj*^s zOHP(MsK|isJR90F^~?c~`}2)VNdONo{{_51lT)xD>XlmM72-d^K_eI;+kLEkwF3e8|9**ybSlQey^ZaM?JC4On|EFq8n3qaFc_=>Dn za97EJLNQJf`W#Eqf8Nx7oxadQUXe915G;9Wbh-Rbha0BDf5?yjbPOy(+855 z`+`=UwIVK9f$Ui}c$f*j(5%}XQVXv=7SP>aR8hG82_SC##l~>mlh9wt-B=Vv15;0S z9y><NO?kzG z*h?4b%Z!bfv8?{)V8(I)SOU%y2Ig5x8-LX-_u!)kts9`u{bB%?WhiB@kg5v3|LoS6 zErF3GCybbFHVBBCKL22AJ{0i>GEUwi(Z>Z9oJ~f8vOtFNNpm<+l@|ohh;WGENfKBVelHQv69#P3(XKc4y}u?CAIZk!DNI;Uas%lFUC+dF~u3#kQV|wiBrvt_&2!=VMgC#-ZqIzmxrVM zr1Y)lhjRR44m8vMs`xlzg1*G|g!>Q*W1) z5FQ)rQvd_h&J8$cSudYn?z~)(*cVH{u#GSfI7JAD`;{CVJGX9+LJ;F`f)~IG&|h+E z(%w>OcCQZvG)4ei4;fyR%!;BD1+jrLbNbMCG!kUzz5uQ$sdT<`OxUXcS1j$Ch8OT& z5d_gz0#y4##75u#=cp~%8-}BQ<&1h2fjG=TPB)U&k;&rN?_FQN+y%`0 z!%Ewsym4a}aD@+I?K*m_x(94!7NGfa#*qijN}<*%)HxEgSW(f_Vz3>6Ef9J6s&=?p z&#{(EG5i$?phO%KRyF<0+7L8^9Z)7@X?kyY|&Yzb2yCsAm|mkWB?HZ+zFCJ2rL2}H6M0g{o7 zoPKFNm0cJlak;TBU?s>-xe$Y(X#sneMHIQ>cGnT|jsEw(O)HMssuZV4}NjyE3670fTQz0dmE=olG*%gLr1#Ts4!{Aa`TO zi+jFxjzf`$`%BV#F7dQ@*r#kyqbH}G8{fNxO1vR;y*|6rH z`GJLRAVzXq2znHejGIW^Yu;>(h^2Kon5e8e{N_IyZ-^e}{hIFSx-J`8T>ox~UoBeOpjOT!KbK}|% zHqW-AHo`|ZqfnDBTtYKOC{d@RoOm3U%e3mJN{whprQ?aMhuTF6QqQQ^ucEc5K^l|8 z_|)64+7>I~ufZF=vek-{^0>^|*hlGqux%?%dP$DgYxW{se%3AYL?jkKe6PSZ^>8B8 z8;9P#JC12G`WwwvK5VsD8XgT|qP>>mE}H8e^hg&k5PkF_BGc}6Uek7JI__oq@XM<2 zyLQHf+M{PBDuoC0cuLhiEs}1&C5BjR1n7{E&L@<5u^V*9C=Iq_V&YBajh|Bd*cS}=8=;`e2!CP#Bc@9POf9V2HxZl z`f0Emvk|Q=#3`VMZF+lL)Qb?B;?;mC-V!7Gvg_FoV}fvx)c2f!&JRtZlLv^J!kdAl z{-1^g`paN1gygTO!y6s4&CK?B>m2tY4+pc!RM@X$fh{1_r6S-F$l@`YNK4BL5R%0G zo`Sw`a-M?vvGIZF1lL5@#|(zsDaFj)hiAxwXm0H~KyQR3$8v4e@Cr&T&w#%m+$KdnQi`rMNCZLE2c7>L8pZ;0ZoOm@&S9YCBgTMoltT&*ZUsC-( zfGZ)b2AMvNTt=dN5P#38A4oFJBw2+i- z$~!RVBDWu{ z|1+e$P3Z}QWc-sW(CM=YxS@2ASz$Ca*#4f)?HQcvra;qGq5^Zn>1KT?`cljpc*K6T`zvTY$VgPZO<%1qv)w@V=w?Lbgo=vG_qy%J zry@LSMj0_j)!ft8l#>w*TSkF~KhiFvnA`zY!@-jEz*Z+_8KCLJi*GkNnB z{`lm5jE;g|ffu`I?thWtsZ+1rE#{|!*uFSx=&KrJs;i4BTM^Te9gBK8zXJNGk%nhf zQL^i&-R!g53?al z3!UfQZKHXp7t)G`Hls@R#Y8L3mg1;1^Z&!!6lQ}{thTp0HUv^_20^bOt@Q-<1k`@% zxMUyev*oCqBI!SESA*TnAH|3!=PNU)^BV!Z-3n3t47-uIYHyDI{N6O>F-*Mf*{1K| zm&~o%nW-NgruE4K(oX&|hT#vGNm;1>A8x3LThU=l-QObp0@wL(HLwP4wJqEyYDZH9 z889;k)k#TgJjL#JQJo@*uRm(rc{i@UG?>Ia5XmG2V_*BXTj~QQ<)~h=c;S%J+t{4A z=FEo&z?1?gUIK$o#QqfCLqs}OGAGxAE&O>CWW)XOzcM;~yaLl9Dz^XN&E5nd3EW(f zy_5o>_0^~TvIO0%^m0?tNp#0r*THA=hzb2>nbB$g?UR)*cYOJ~bEGT^fOtl@A7uSy zaS>o65-qgqy!&%|UAB?t1YO0JEDAfvH@93@TUR<;EW~fZc;o`Oxn6a+dW=YN;hM8? zHW~cX0hz5Cy%iGZtu-;h4L`Qh%B~HMS$6V|NKoOnuV zDvcwqRl}_`qZm!aEkEM@@t*m+)J`u-`{rKee+pLHjHI>-jyBTp)%jc{8V@jU+TXsh zg3ptW;Yt(2XR>i$7J2i=x=-!JQZKfW79e_$dzW4UVLfAJ9MD~LYq1Oz?$;%Uz#h-N zFVZVq(%pw7=6m?xjAef2-TDOO6Glq^G4G?>;ZCV}f1$hovzQz}s|~dUXbQet7H2m1 z)cIWPn@F0qF2eym(3o}{AQd*(s+DL3Df7D;EWG&8Pxt6L;NMW~6=i3~y?;c6H@N;7ekGK%fh?6QNHqd z|FvaNlgj7rCy()-a@t6)lUFGZ(Oepd)3@Y9iCxL>v!`e24!KWZB;;}zM`qEwP3se>+;|>a6MVzqo44wFz(ueCGiZ51aphwC>F!rr|_saE5x1 z>wiS1#On2c zdxy>~m=KbIIt8DMKeUWKkj=INnq`Xw5G=J{754zvQhOEEH`cC&;kT%DWarWKh&}PH ziI=TBTz+`=-e@RsYPJ4;0?hC!0F?1p%)WF!=gV$2&n}4X5dt_iu*}}AuZZ#O$28~> zoM4d4S;we<3Pmyu_FbAlTDC@Cxtf=V`K3_!g?W%x74Wu5qCq;S`N}{cdGj%=u!Q^7 zTNI@FbLVlP8_UhCJ00iVacW*IYl41UNU_|Jm9D+)-YG1J0A7=lAE!e;`F{ypVBs-4 znSjAj{P+)D3;6{IF^U96v5Ny51PtTA0ejwH@8cV_T0k7+Zwdh;zr4j2FC6*(nY+zj ztY-Osoa(X($p#D~8{pSoNj-o^8EgaKuDLI|Ix6N8Rc+U7M;>P^N=1XVw7sB?Z zJAF7Ymu`S;{jjBG8juX)%!SfslM}lP){?!z?t`q1ffkRuL&c)Aty0M!CMqRq;BS~} z`DdUg?gBG$#GHvpK)ma8@OYhM zB={Bh20b0R5tvIFEQ42(t&j_gm;jKjDswlvl#ut&7l=Qa?c$_?91RR1u!vr_=m6u= zx9pdwp;GHLz#S<)8_KzopA9BbnOafu?dB!GMvz)q6Xos-m%M<e^uY_)yxPaTy(nCBY5{p7cOfAfWD$i!9T-8y28LZt@`Oqi4;&~EjOE0ge zJ8;hS^;uq(<;u)sOx!HWdLLm;!hK#QI$ZSPOiyC_u>0~@^m{u_1_Gn(7_xqSw58J< ztps?CGf^Mj8s{BC!Tl6j;y>^SE|FiXR25{1z&Kv00dHVbEXYH%Sq(Be+rY_asZYCA z&!b6MrZ&66#z1-+i?RrFm-dEcj9;bfJz@Y?9aFimw4;?`?rXo&>dC=06if+My+|UJ z0rdLSm}rWeu{NiegI2?<5}+*7WJ`STZsEDqN5JaWuLRh-DRMbMML-Xk33dBtQZCHA-$3^ zXxnx#o={u}K43Yx%=azLQ0dG=Rec~r1lvd97It&8LPpPG;hv;5nyv+GS6{ zvuyXf=9yUq5Y-fUX8E_T7pT>Gbf|2I{0KrWRMP75i^ib$`1POw`9c~y=fpz^9o3? z3#(N*gkz_*Tj%5if1n%~mVk}(Z>Qk(X$3BjXi_TCTsY{h(p^;7mI@z`ffoTQ4?uGl zQlUpmHdcPD_!>avIhqE%ieMt8u-VR;t}6&5_RDmh+^IPfSZ)>p(u0-i@+HbAyYV2v z1DB~0gZ~f21%uT#bapW#i z)@jAg5J9^L`W4Xp*l^d}7;G+>c!P|=86zDw0V&1as75P?)&}izZ49l_rVd*UV%h6-d}7yFSn`oAV{?hX-R|Jduvm_ zqjNeQ5lekEbx%@oee(K`-B>E(hOX6z)lCh*={A1_QFlh>6g$-^tSIi9j}q(|w(d!0 z;rx7a7K5#y3WQ4mfj))QTQM$9Spo33HFpOyNbC2MHwJ#6NGvyoET{lAV1wb8PeKjc z2EN&!p!A2PdaF3Ig;57kdMjYfCNQW?0K?CxqybUgT(Yi6Kh3%0T%H2Fe(2?+xU$Z8 z6fvU~JfSaVaJ=)6$k8yMt{ev^)84n?-&KSD+=vUq^)(sCGRyQ^R4H^FJ^KME(uCm= zCi>`RU5PkmnUxME)15<=_C|ycM1(7iz~ec|}H(wV)mr-!(sC9=W_kXbGrI zZ$TNO!7a&x)-&6g-b>z+le*q@-3S_shgIL;o*M@Z}h9#v*BK)T6pBtzvds5YB$)VuF9dB)FEZsG^YS`mUVFBOb z@YsKQ8qL!q!+idcR>FxMaPR=P1L4BD1N57y9VM63of%w_sruq{@u@+hz25%UG{Bv5 z$p7tc1p6t$%|uHuA$NwpWil8O{Xn=4jC%89_$S2(M9Dddw}v)1a}% zI?we-FAlYH7bsYwkJar0pV9^&BTv4SD-!O<$*A-4Pmd^tCh$;hmdG{OydmI*b$HP*?~S$T1m)$i6%AiVNVX zg^68TPlBDK;`fv8JEHZc&Tp%5`YXLlF)-jM`F%y&J(&1|0x%2`?3~QzdDl;UR4J1( z?bEa=y!2T9g9~`^^a=-Ua9rlyf${Qn=(wblOO}a}7u7miXy&_`Q3=gZ_!*kM2W081G6DsgtXRdg}T=<5x zD8G>9;933Kl+cN}Mys$Uo^hAkYC znNqI5o=d^Ic>1}%o(WJz!8hqHLC}nnTuTR&`8a#h# + + + + + + diff --git a/UI/index.html b/UI/index.html index 5b5fc810..bcb23b64 100644 --- a/UI/index.html +++ b/UI/index.html @@ -4,6 +4,7 @@ + Date: Sat, 19 Jul 2025 22:52:28 +0300 Subject: [PATCH 121/152] successful build with logo --- UI/vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/UI/vite.config.ts b/UI/vite.config.ts index ba3d0846..f4533cd9 100644 --- a/UI/vite.config.ts +++ b/UI/vite.config.ts @@ -4,5 +4,6 @@ import svgr from "vite-plugin-svgr"; // https://vitejs.dev/config/ export default defineConfig({ + base: './', plugins: [react(), svgr()], }); From cad4309a5ec23422f8924b59a6b8ef7a72102738 Mon Sep 17 00:00:00 2001 From: witch2256 Date: Sun, 20 Jul 2025 14:34:56 +0300 Subject: [PATCH 122/152] try to fix login and registration(wrong link) --- UI/src/components/pages/auth.jsx | 2 +- UI/src/components/pages/register.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index 66885e7e..d518bfd6 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -66,7 +66,7 @@ const Auth = () => { return; } try { - const response = await fetch("http://auth:8080/api/auth/login", { + const response = await fetch("https://auth:8080/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ login, password }), diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index 2b08a2c1..326631ab 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -165,7 +165,7 @@ const Reg = () => { if (!hasErrors) { try { - const response = await fetch("http://auth:8080/api/auth/register", { + const response = await fetch("https://auth:8080/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ From d87d34529bb51523fe961445fc60043be5aab849 Mon Sep 17 00:00:00 2001 From: witch2256 Date: Sun, 20 Jul 2025 14:47:17 +0300 Subject: [PATCH 123/152] try to fix login and registration(wrong link)v.2 --- UI/src/components/pages/auth.jsx | 2 +- UI/src/components/pages/register.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/UI/src/components/pages/auth.jsx b/UI/src/components/pages/auth.jsx index d518bfd6..1b51879f 100644 --- a/UI/src/components/pages/auth.jsx +++ b/UI/src/components/pages/auth.jsx @@ -66,7 +66,7 @@ const Auth = () => { return; } try { - const response = await fetch("https://auth:8080/api/auth/login", { + const response = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ login, password }), diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index 326631ab..3d8489ed 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -165,7 +165,7 @@ const Reg = () => { if (!hasErrors) { try { - const response = await fetch("https://auth:8080/api/auth/register", { + const response = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ From 57f8929a3b8d44b9fd2d2ed197e97bc9cb9f1379 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 15:36:03 +0300 Subject: [PATCH 124/152] id generation performed properly --- UI/src/components/pages/mainPage.jsx | 6 +----- UI/src/components/utils/customBlockUtils.js | 8 ++------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 19d35a50..d31ae7d7 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -67,7 +67,6 @@ import { handleTabSwitch as handleTabSwitchUtil } from "../utils/handleTabSwitch import { getEditableNode } from "../utils/getEditableNode.js"; import { handleNameChange } from "../utils/handleNameChange.js"; import CreateCustomBlockModal from "./mainPage/CreateCustomBlockModal.jsx"; -import { createCustomBlock, saveCustomBlock } from "../utils/customBlockUtils.js"; export const SimulateStateContext = createContext({ simulateState: "idle", @@ -138,7 +137,6 @@ export default function Main() { const ignoreChangesRef = useRef(false); const [modalOpen, setModalOpen] = useState(false); - const [mode, setMode] = useState("fromSelected"); const [nodesCustom, setNodesCustom] = useNodesState([]); const [edgesCustom, setEdgesCustom] = useEdgesState([]); @@ -157,18 +155,16 @@ export default function Main() { (event) => { loadCircuitUtil(event, setNodesCustom, setEdgesCustom); setModalOpen(true); - setMode("fromFile"); }, [setNodesCustom, setEdgesCustom], ); const onCreateCustom = useCallback( - (event) => { + () => { const selectedElements = getSelectedElements(); setNodesCustom(selectedElements.nodes); setNodesCustom(selectedElements.edges); setModalOpen(true); - setMode("fromFile"); }, [setNodesCustom, setEdgesCustom], ); diff --git a/UI/src/components/utils/customBlockUtils.js b/UI/src/components/utils/customBlockUtils.js index ebf2c150..6a7ddd40 100644 --- a/UI/src/components/utils/customBlockUtils.js +++ b/UI/src/components/utils/customBlockUtils.js @@ -1,9 +1,5 @@ /*TODO: Add tests*/ -export const generateCustomBlockId = () => { - const timestamp = Date.now().toString(36); - const random = Math.random().toString(36).substring(2, 10); - return `custom_${timestamp}_${random}`; -}; +import { generateId } from "./generateId.js"; export const createCustomBlock = (nodes, edges, blockName) => { if (!Array.isArray(nodes)) throw new Error("Invalid nodes: must be an array"); @@ -34,7 +30,7 @@ export const createCustomBlock = (nodes, edges, blockName) => { }); return { - id: generateCustomBlockId(), + id: generateId(), name: blockName, inputNodes, outputNodes, From c037085e6cf9e1fc8796493fa1711a676db1059f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 12:36:30 +0000 Subject: [PATCH 125/152] Automated formatting --- UI/assets/toolbar-icons.jsx | 28 +----- UI/src/CSS/toolbar.css | 1 - .../codeComponents/PaneContextMenu.jsx | 32 +++--- UI/src/components/pages/mainPage.jsx | 15 ++- .../pages/mainPage/createCustomBlockModal.jsx | 30 +++--- UI/src/components/pages/mainPage/toolbar.jsx | 5 +- .../unit tests/customBlockUtils.unit.test.js | 98 +++++++++++-------- UI/src/components/utils/customBlockUtils.js | 34 ++++--- UI/vite.config.ts | 2 +- 9 files changed, 127 insertions(+), 118 deletions(-) diff --git a/UI/assets/toolbar-icons.jsx b/UI/assets/toolbar-icons.jsx index a9321902..f65d2bf1 100644 --- a/UI/assets/toolbar-icons.jsx +++ b/UI/assets/toolbar-icons.jsx @@ -131,28 +131,10 @@ export const IconToolbarCustomBlock = ({ SVGClassName }) => ( fill="none" xmlns="http://www.w3.org/2000/svg" > - + - - - + + + -) \ No newline at end of file +); diff --git a/UI/src/CSS/toolbar.css b/UI/src/CSS/toolbar.css index 17214261..780fa8da 100644 --- a/UI/src/CSS/toolbar.css +++ b/UI/src/CSS/toolbar.css @@ -188,4 +188,3 @@ .dropdown-item:hover { background-color: var(--select-2); } - diff --git a/UI/src/components/codeComponents/PaneContextMenu.jsx b/UI/src/components/codeComponents/PaneContextMenu.jsx index 220534b0..c59a448e 100644 --- a/UI/src/components/codeComponents/PaneContextMenu.jsx +++ b/UI/src/components/codeComponents/PaneContextMenu.jsx @@ -2,19 +2,19 @@ import React, { useCallback } from "react"; import { useReactFlow } from "@xyflow/react"; export default function PaneContextMenu({ - copyElements, - pasteElements, - cutElements, - selectedElements, - clipboard, - onClose, - top, - left, - right, - bottom, - onAddCustomCircuit, // New prop to open modal - ...props - }) { + copyElements, + pasteElements, + cutElements, + selectedElements, + clipboard, + onClose, + top, + left, + right, + bottom, + onAddCustomCircuit, // New prop to open modal + ...props +}) { const { setNodes, setEdges } = useReactFlow(); const deleteSelectedElements = useCallback(() => { @@ -27,8 +27,8 @@ export default function PaneContextMenu({ (edge) => !selectedEdgeIds.has(edge.id) && !selectedNodeIds.has(edge.source) && - !selectedNodeIds.has(edge.target) - ) + !selectedNodeIds.has(edge.target), + ), ); }, [selectedElements, setNodes, setEdges]); @@ -76,4 +76,4 @@ export default function PaneContextMenu({
); -} \ No newline at end of file +} diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index d31ae7d7..041d2c1d 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -159,15 +159,12 @@ export default function Main() { [setNodesCustom, setEdgesCustom], ); - const onCreateCustom = useCallback( - () => { - const selectedElements = getSelectedElements(); - setNodesCustom(selectedElements.nodes); - setNodesCustom(selectedElements.edges); - setModalOpen(true); - }, - [setNodesCustom, setEdgesCustom], - ); + const onCreateCustom = useCallback(() => { + const selectedElements = getSelectedElements(); + setNodesCustom(selectedElements.nodes); + setNodesCustom(selectedElements.edges); + setModalOpen(true); + }, [setNodesCustom, setEdgesCustom]); const handleCreateFromCurrent = (customBlock) => { // Handle custom block creation diff --git a/UI/src/components/pages/mainPage/createCustomBlockModal.jsx b/UI/src/components/pages/mainPage/createCustomBlockModal.jsx index 2c19754a..f0eed2c5 100644 --- a/UI/src/components/pages/mainPage/createCustomBlockModal.jsx +++ b/UI/src/components/pages/mainPage/createCustomBlockModal.jsx @@ -1,16 +1,19 @@ import React, { useState } from "react"; import { IconCloseCross } from "../../../../assets/ui-icons.jsx"; import { showToastError } from "../../codeComponents/logger.jsx"; -import { createCustomBlock, saveCustomBlock } from "../../utils/customBlockUtils"; +import { + createCustomBlock, + saveCustomBlock, +} from "../../utils/customBlockUtils"; export default function CreateCustomBlockModal({ - isOpen, - onClose, - nodes, - edges, - onCreateFromFile, - onCreateFromCurrent, - }) { + isOpen, + onClose, + nodes, + edges, + onCreateFromFile, + onCreateFromCurrent, +}) { const [blockName, setBlockName] = useState(""); const [error, setError] = useState(""); @@ -33,7 +36,7 @@ export default function CreateCustomBlockModal({ console.error("Error creating block:", err); setError(`Error: ${err.message}`); } - } + }; if (!isOpen) return null; @@ -42,7 +45,7 @@ export default function CreateCustomBlockModal({

Create custom block

@@ -55,7 +58,10 @@ export default function CreateCustomBlockModal({ placeholder="New custom block name" required /> - {error &&

{error}

} @@ -65,4 +71,4 @@ export default function CreateCustomBlockModal({
); -} \ No newline at end of file +} diff --git a/UI/src/components/pages/mainPage/toolbar.jsx b/UI/src/components/pages/mainPage/toolbar.jsx index 6986aafc..3dde3db0 100644 --- a/UI/src/components/pages/mainPage/toolbar.jsx +++ b/UI/src/components/pages/mainPage/toolbar.jsx @@ -214,10 +214,7 @@ export default function Toolbar({ onClick={handleUploadClick} title={"Upload"} > - + { localStorage.clear(); jest.clearAllMocks(); }); -describe('generateCustomBlockId', () => { - it('should generate a unique id with correct format', () => { +describe("generateCustomBlockId", () => { + it("should generate a unique id with correct format", () => { const id = generateCustomBlockId(); expect(id).toMatch(/^custom_[a-z0-9]+_[a-z0-9]{8}$/); }); - it('should generate different IDs on consecutive calls', () => { + it("should generate different IDs on consecutive calls", () => { const id1 = generateCustomBlockId(); const id2 = generateCustomBlockId(); expect(id1).not.toEqual(id2); }); }); -describe('createCustomBlock', () => { +describe("createCustomBlock", () => { const nodes = [ - { id: '1', type: 'inputNodeSwitch', name: 'Switch A' }, - { id: '2', type: 'inputNodeButton' }, - { id: '3', type: 'outputNodeLed', name: 'LED' }, - { id: '4', type: 'logicNode' }, + { id: "1", type: "inputNodeSwitch", name: "Switch A" }, + { id: "2", type: "inputNodeButton" }, + { id: "3", type: "outputNodeLed", name: "LED" }, + { id: "4", type: "logicNode" }, ]; - const edges = [{ id: 'e1', source: '1', target: '3' }]; + const edges = [{ id: "e1", source: "1", target: "3" }]; - it('should create a custom block with inputs and outputs', () => { - const block = createCustomBlock(nodes, edges, 'My Block'); + it("should create a custom block with inputs and outputs", () => { + const block = createCustomBlock(nodes, edges, "My Block"); - expect(block).toHaveProperty('id'); - expect(block).toHaveProperty('name', 'My Block'); + expect(block).toHaveProperty("id"); + expect(block).toHaveProperty("name", "My Block"); expect(block.inputs).toEqual( expect.arrayContaining([ - expect.objectContaining({ id: '1', name: 'Switch A' }), - expect.objectContaining({ id: '2' }), - ]) + expect.objectContaining({ id: "1", name: "Switch A" }), + expect.objectContaining({ id: "2" }), + ]), ); expect(block.outputs).toEqual( expect.arrayContaining([ - expect.objectContaining({ id: '3', name: 'LED' }), - ]) + expect.objectContaining({ id: "3", name: "LED" }), + ]), ); expect(block.originalSchema.nodes).toBe(nodes); expect(block.originalSchema.edges).toBe(edges); }); - it('should throw error if nodes or edges are not arrays', () => { - expect(() => createCustomBlock(null, [], 'Block')).toThrow(); - expect(() => createCustomBlock([], null, 'Block')).toThrow(); + it("should throw error if nodes or edges are not arrays", () => { + expect(() => createCustomBlock(null, [], "Block")).toThrow(); + expect(() => createCustomBlock([], null, "Block")).toThrow(); }); }); -describe('saveCustomBlock & loadCustomBlocks', () => { - it('should save and load a block correctly', () => { - const block = { id: 'test-id', name: 'Test', inputs: [], outputs: [], originalSchema: {} }; +describe("saveCustomBlock & loadCustomBlocks", () => { + it("should save and load a block correctly", () => { + const block = { + id: "test-id", + name: "Test", + inputs: [], + outputs: [], + originalSchema: {}, + }; saveCustomBlock(block); const loaded = loadCustomBlocks(); @@ -70,42 +76,54 @@ describe('saveCustomBlock & loadCustomBlocks', () => { expect(loaded[0]).toEqual(block); }); - it('should handle malformed JSON gracefully', () => { - localStorage.setItem('customBlocks', 'invalid_json'); + it("should handle malformed JSON gracefully", () => { + localStorage.setItem("customBlocks", "invalid_json"); const loaded = loadCustomBlocks(); expect(loaded).toEqual([]); }); }); -describe('deleteCustomBlock', () => { - it('should delete the specified block', () => { - const block = { id: 'to-delete', name: 'To Delete', inputs: [], outputs: [], originalSchema: {} }; +describe("deleteCustomBlock", () => { + it("should delete the specified block", () => { + const block = { + id: "to-delete", + name: "To Delete", + inputs: [], + outputs: [], + originalSchema: {}, + }; saveCustomBlock(block); expect(loadCustomBlocks()).toHaveLength(1); - const result = deleteCustomBlock('to-delete'); + const result = deleteCustomBlock("to-delete"); expect(result).toBe(true); expect(loadCustomBlocks()).toHaveLength(0); }); - it('should return false if deletion fails', () => { - localStorage.setItem('customBlocks', 'bad_json'); - const result = deleteCustomBlock('any-id'); + it("should return false if deletion fails", () => { + localStorage.setItem("customBlocks", "bad_json"); + const result = deleteCustomBlock("any-id"); expect(result).toBe(false); }); }); -describe('findCustomBlockById', () => { - it('should return the correct block by ID', () => { - const block = { id: 'block123', name: 'FindMe', inputs: [], outputs: [], originalSchema: {} }; +describe("findCustomBlockById", () => { + it("should return the correct block by ID", () => { + const block = { + id: "block123", + name: "FindMe", + inputs: [], + outputs: [], + originalSchema: {}, + }; saveCustomBlock(block); - const found = findCustomBlockById('block123'); + const found = findCustomBlockById("block123"); expect(found).toEqual(block); }); - it('should return undefined for nonexistent ID', () => { - const found = findCustomBlockById('missing'); + it("should return undefined for nonexistent ID", () => { + const found = findCustomBlockById("missing"); expect(found).toBeUndefined(); }); }); diff --git a/UI/src/components/utils/customBlockUtils.js b/UI/src/components/utils/customBlockUtils.js index 6a7ddd40..18fb90a5 100644 --- a/UI/src/components/utils/customBlockUtils.js +++ b/UI/src/components/utils/customBlockUtils.js @@ -5,27 +5,32 @@ export const createCustomBlock = (nodes, edges, blockName) => { if (!Array.isArray(nodes)) throw new Error("Invalid nodes: must be an array"); if (!Array.isArray(edges)) throw new Error("Invalid edges: must be an array"); - const inputNodes = nodes.filter(node => - node.type === "inputNodeSwitch" || node.type === "inputNodeButton" + const inputNodes = nodes.filter( + (node) => + node.type === "inputNodeSwitch" || node.type === "inputNodeButton", ); - const outputNodes = nodes.filter(node => - node.type === "outputNodeLed" - ); + const outputNodes = nodes.filter((node) => node.type === "outputNodeLed"); if (inputNodes.length === 0 || outputNodes.length === 0) { - throw new Error("Custom block must have at least one input and one output pin"); + throw new Error( + "Custom block must have at least one input and one output pin", + ); } - inputNodes.forEach(node => { + inputNodes.forEach((node) => { if (!node.name) { - throw new Error(`Input \"${node.type.replace("inputNode", "")}\" must have a name`); + throw new Error( + `Input \"${node.type.replace("inputNode", "")}\" must have a name`, + ); } }); - outputNodes.forEach(node => { + outputNodes.forEach((node) => { if (!node.name) { - throw new Error(`Output \"${node.type.replace("outputNode", "")}\" must have a name`); + throw new Error( + `Output \"${node.type.replace("outputNode", "")}\" must have a name`, + ); } }); @@ -43,8 +48,13 @@ export const createCustomBlock = (nodes, edges, blockName) => { */ export const saveCustomBlock = (customBlock) => { try { - const savedBlocks = JSON.parse(localStorage.getItem("customBlocks") || "[]"); - localStorage.setItem("customBlocks", JSON.stringify([...savedBlocks, customBlock])); + const savedBlocks = JSON.parse( + localStorage.getItem("customBlocks") || "[]", + ); + localStorage.setItem( + "customBlocks", + JSON.stringify([...savedBlocks, customBlock]), + ); } catch (error) { console.error("Failed to save custom block:", error); } diff --git a/UI/vite.config.ts b/UI/vite.config.ts index f4533cd9..86c5754b 100644 --- a/UI/vite.config.ts +++ b/UI/vite.config.ts @@ -4,6 +4,6 @@ import svgr from "vite-plugin-svgr"; // https://vitejs.dev/config/ export default defineConfig({ - base: './', + base: "./", plugins: [react(), svgr()], }); From cb44f1604bdb6bfa101f65c7d0bd6930f75aa989 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 15:45:21 +0300 Subject: [PATCH 126/152] custom node can be created from context menu --- .../codeComponents/PaneContextMenu.jsx | 4 ++-- UI/src/components/pages/mainPage.jsx | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/UI/src/components/codeComponents/PaneContextMenu.jsx b/UI/src/components/codeComponents/PaneContextMenu.jsx index c59a448e..c4eda894 100644 --- a/UI/src/components/codeComponents/PaneContextMenu.jsx +++ b/UI/src/components/codeComponents/PaneContextMenu.jsx @@ -12,7 +12,7 @@ export default function PaneContextMenu({ left, right, bottom, - onAddCustomCircuit, // New prop to open modal + onCreateCustom, ...props }) { const { setNodes, setEdges } = useReactFlow(); @@ -68,7 +68,7 @@ export default function PaneContextMenu({ +
+
+ {item.gates.map((node) => ( +
onDragStart(e, node.id)} + title={node.label} + > + +
+ ))}
- ))} -
-
- - ))} - +
+ + ))} + +
-
); -} +} \ No newline at end of file diff --git a/UI/src/components/pages/mainPage/customCircuit.jsx b/UI/src/components/pages/mainPage/customCircuit.jsx index 18943356..e1266239 100644 --- a/UI/src/components/pages/mainPage/customCircuit.jsx +++ b/UI/src/components/pages/mainPage/customCircuit.jsx @@ -1,17 +1,53 @@ -import React, { useState } from "react"; +import React, { createContext, useState, useContext, useEffect } from 'react'; import { IconCloseCross } from "../../../../assets/ui-icons.jsx"; -import toast from "react-hot-toast"; -import { showToastError } from "../../codeComponents/logger.jsx"; - -export default function CreateCustomBlock({ - nodes, - edges, - onCreateFromFile, // Колбэк при создании из файла (вы реализуете) - onCreateFromCurrent, // Колбэк при создании из схемы (вы реализуете) -}) { +import {LOG_LEVELS, showToast, showToastError} from "../../codeComponents/logger.jsx"; + +// Контекст для управления кастомными блоками +const CustomBlocksContext = createContext(); + +export const CustomBlocksProvider = ({ children }) => { + const [customBlocks, setCustomBlocks] = useState([]); + + // Загрузка блоков из localStorage при инициализации + useEffect(() => { + const blocks = JSON.parse(localStorage.getItem('customBlocks') || '[]'); + setCustomBlocks(blocks); + }, []); + + // Сохранение блоков в localStorage при изменении + useEffect(() => { + localStorage.setItem('customBlocks', JSON.stringify(customBlocks)); + }, [customBlocks]); + + // Добавление нового блока + const addBlock = (block) => { + setCustomBlocks(prev => [...prev, block]); + }; + + // Удаление блока + const deleteBlock = (blockId) => { + setCustomBlocks(prev => prev.filter(block => block.id !== blockId)); + }; + + return ( + + {children} + + ); +}; + +export const useCustomBlocks = () => useContext(CustomBlocksContext); + +// Компонент для создания кастомных блоков +export default function CreateCustomBlock({ nodes, edges }) { const [isModalOpen, setIsModalOpen] = useState(false); const [blockName, setBlockName] = useState(""); const [error, setError] = useState(""); + const { addBlock } = useCustomBlocks(); const handleCreateFromCurrent = () => { if (!blockName.trim()) { @@ -20,181 +56,84 @@ export default function CreateCustomBlock({ } try { - // Создаем кастомный блок из текущей схемы - const customBlock = createCustomBlock(nodes, edges, blockName.trim()); - - // Сохраняем в localStorage - saveCustomBlock(customBlock); + // Создаем кастомный блок + const customBlock = { + id: `custom_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 10)}`, + name: blockName.trim(), + inputs: nodes.filter(node => + node.type === "inputNodeSwitch" || node.type === "inputNodeButton" + ).map(node => ({ + id: node.id, + name: node.name || `input_${node.id.slice(0, 4)}` + })), + outputs: nodes.filter(node => + node.type === "outputNodeLed" + ).map(node => ({ + id: node.id, + name: node.name || `output_${node.id.slice(0, 4)}` + })), + originalSchema: { nodes, edges } + }; + + addBlock(customBlock); - // Сбрасываем состояние setBlockName(""); setError(""); setIsModalOpen(false); - // Вызываем колбэк (если нужна дополнительная логика) - if (onCreateFromCurrent) { - onCreateFromCurrent(customBlock); - } - - alert(`Блок "${blockName}" успешно создан!`); + showToast(`Block "${blockName}" created successfully!`, '✅', LOG_LEVELS.ERROR); } catch (err) { - console.error("Ошибка при создании блока:", err); - setError(`Ошибка: ${err.message}`); - } - }; - - const handleCreateFromFile = () => { - setIsModalOpen(false); - if (onCreateFromFile) { - onCreateFromFile(); + console.error("Block creation error:", err); + setError(`Error: ${err.message}`); } }; return ( -
- - - {isModalOpen && ( -
-
-

Create custom block

- - - -
- - -
+
+ + + {isModalOpen && ( +
+
+

Create custom block

+ -
- setBlockName(e.target.value)} - placeholder="New custom block name" - required - /> - {error &&

{error}

} +
+ + +
+ + +
+ setBlockName(e.target.value)} + placeholder="New custom block name" + required + /> + {error &&

{error}

} +
+
-
-
- )} -
+ )} +
); -} - -const generateCustomBlockId = () => { - const timestamp = Date.now().toString(36); - const random = Math.random().toString(36).substring(2, 10); - return `custom_${timestamp}_${random}`; -}; - -export const createCustomBlock = (nodes, edges, blockName) => { - // Валидация входных данных - if (!Array.isArray(nodes)) { - throw new Error("Invalid nodes: must be an array"); - } - - if (!Array.isArray(edges)) { - throw new Error("Invalid edges: must be an array"); - } - - // Фильтрация входных нод - const inputs = nodes.reduce((acc, node) => { - if (node.type === "inputNodeSwitch" || node.type === "inputNodeButton") { - acc.push({ - id: node.id, - name: node.name || `input_${Math.floor(Math.random() * 10000)}`, // Случайный номер - }); - } - return acc; - }, []); - - // Фильтрация выходных нод - const outputs = nodes.reduce((acc, node) => { - if (node.type === "outputNodeLed") { - acc.push({ - id: node.id, - name: node.name || `output_${Math.floor(Math.random() * 10000)}`, // Случайный номер - }); - } - return acc; - }, []); - - return { - id: generateCustomBlockId(), - name: blockName, - inputs, - outputs, - originalSchema: { nodes, edges }, // Сохраняем полную схему - }; -}; - -/** - * Сохраняет кастомный блок в localStorage - */ -export const saveCustomBlock = (customBlock) => { - try { - const savedBlocks = JSON.parse( - localStorage.getItem("customBlocks") || "[]", - ); - const updatedBlocks = [...savedBlocks, customBlock]; - localStorage.setItem("customBlocks", JSON.stringify(updatedBlocks)); - } catch (error) { - console.error("Failed to save custom block:", error); - } -}; - -/** - * Загружает все кастомные блоки из localStorage - */ -export const loadCustomBlocks = () => { - try { - return JSON.parse(localStorage.getItem("customBlocks") || "[]"); - } catch (error) { - console.error("Failed to load custom blocks:", error); - return []; - } -}; - -/** - * Удаляет кастомный блок по ID - */ -export const deleteCustomBlock = (blockId) => { - try { - const savedBlocks = JSON.parse( - localStorage.getItem("customBlocks") || "[]", - ); - const updatedBlocks = savedBlocks.filter((block) => block.id !== blockId); - localStorage.setItem("customBlocks", JSON.stringify(updatedBlocks)); - return true; - } catch (error) { - console.error("Failed to delete custom block:", error); - return false; - } -}; - -/** - * Находит кастомный блок по ID - */ -export const findCustomBlockById = (blockId) => { - const blocks = loadCustomBlocks(); - return blocks.find((block) => block.id === blockId); -}; +} \ No newline at end of file From 241619cead8d21292f34f563f5812650e81af8c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 14:44:25 +0000 Subject: [PATCH 130/152] Automated formatting --- UI/src/components/codeComponents/nodes.js | 3 +- UI/src/components/pages/mainPage.jsx | 527 +++++++++--------- .../pages/mainPage/circuitsMenu.jsx | 166 +++--- .../pages/mainPage/customCircuit.jsx | 142 ++--- 4 files changed, 424 insertions(+), 414 deletions(-) diff --git a/UI/src/components/codeComponents/nodes.js b/UI/src/components/codeComponents/nodes.js index 6781ccea..1b7500cb 100644 --- a/UI/src/components/codeComponents/nodes.js +++ b/UI/src/components/codeComponents/nodes.js @@ -19,6 +19,5 @@ export const nodeTypes = { inputNodeSwitch: InputNodeSwitch, inputNodeButton: InputNodeButton, outputNodeLed: OutputNodeLed, - switchNode: SwitchNode + switchNode: SwitchNode, }; - diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 0f4a08d6..d31f663c 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -69,7 +69,7 @@ import { handleTabSwitch as handleTabSwitchUtil } from "../utils/handleTabSwitch import { getEditableNode } from "../utils/getEditableNode.js"; import { handleNameChange } from "../utils/handleNameChange.js"; import CreateCustomBlock from "./mainPage/customCircuit.jsx"; -import {CustomBlocksProvider} from "./mainPage/customCircuit.jsx"; +import { CustomBlocksProvider } from "./mainPage/customCircuit.jsx"; export const SimulateStateContext = createContext({ simulateState: "idle", @@ -82,7 +82,6 @@ export const NotificationsLevelContext = createContext({ setLogLevel: () => {}, }); - export function useSimulateState() { const context = useContext(SimulateStateContext); if (!context) @@ -568,279 +567,279 @@ export default function Main() { return ( - - -
- -
- - <> - e.preventDefault()} - onInit={setReactFlowInstance} - isValidConnection={isValidConnection} - nodeTypes={nodeTypes} - panOnDrag={panOnDrag} - selectionOnDrag - panOnScroll - snapToGrid - snapGrid={[GAP_SIZE, GAP_SIZE]} - selectionMode={SelectionMode.Partial} - minZoom={0.2} - maxZoom={10} - deleteKeyCode={["Delete", "Backspace"]} - onDelete={deleteSelectedElements} - // onlyRenderVisibleElements={true} - > - - + +
+ - {showMinimap && ( - + + <> + e.preventDefault()} + onInit={setReactFlowInstance} + isValidConnection={isValidConnection} + nodeTypes={nodeTypes} + panOnDrag={panOnDrag} + selectionOnDrag + panOnScroll + snapToGrid + snapGrid={[GAP_SIZE, GAP_SIZE]} + selectionMode={SelectionMode.Partial} + minZoom={0.2} + maxZoom={10} + deleteKeyCode={["Delete", "Backspace"]} + onDelete={deleteSelectedElements} + // onlyRenderVisibleElements={true} + > + - )} - - - {editableNode && ( -
-
- -
-
?
-
- When creating custom circuit, each IO with an export name - will become one of the new circuit's outputs. + + {showMinimap && ( + + )} + + + {editableNode && ( +
+
+ +
+
?
+
+ When creating custom circuit, each IO with an export name + will become one of the new circuit's outputs. +
-
-
- { - if (e.key === "Enter") { +
+ { + if (e.key === "Enter") { + e.preventDefault(); + deselectAll(); + setTimeout(recordHistory, 0); + } + }} + autoFocus + /> + + }} + > + Close + +
-
- )} - - {menu && menu.type === "node" && } - - {menu && menu.type === "edge" && } - - {menu && menu.type === "pane" && ( - } + + {menu && menu.type === "edge" && } + + {menu && menu.type === "pane" && ( + + )} + + - )} - - - - + + + + + Log in + + +
{ + setOpenSettings(false); + }} + /> + +
closeMenu()} /> - - - - - - Log in - - -
{ - setOpenSettings(false); - }} - /> - -
closeMenu()} - /> - -
{ - deselectAll(); - setTimeout(recordHistory, 0); - }} - /> - - { - console.log("создать из файла"); - }} - onCreateFromCurrent={() => { - console.log("создать из текущего"); - }} - /> - - { - setOpenSettings(false); - }} - /> - - - - - handleSimulateClick({ - simulateState, - setSimulateState, - socketRef, - nodes, - edges, - }) - } - undo={undo} - redo={redo} - canUndo={activeTab?.index > 0} - canRedo={activeTab?.index < (activeTab?.history?.length || 1) - 1} - /> - - - + + + ); } diff --git a/UI/src/components/pages/mainPage/circuitsMenu.jsx b/UI/src/components/pages/mainPage/circuitsMenu.jsx index 7df299db..8771ae58 100644 --- a/UI/src/components/pages/mainPage/circuitsMenu.jsx +++ b/UI/src/components/pages/mainPage/circuitsMenu.jsx @@ -14,35 +14,35 @@ import { useCustomBlocks } from "./customCircuit.jsx"; // Импорт конт const CustomBlockIcon = ({ inputs, outputs }) => { return ( -
-
- {inputs.map((input, index) => ( -
- ))} -
-
-
- {outputs.map((output, index) => ( -
- ))} -
+
+
+ {inputs.map((input, index) => ( +
+ ))} +
+
+
+ {outputs.map((output, index) => ( +
+ ))}
+
); }; export default function CircuitsMenu({ - circuitsMenuState, - onDragStart, - spawnCircuit, - }) { + circuitsMenuState, + onDragStart, + spawnCircuit, +}) { const [openIndexes, setOpenIndexes] = useState([]); const { customBlocks } = useCustomBlocks(); // Получаем блоки из контекста const toggleItem = useCallback((index) => { setOpenIndexes((prevIndexes) => - prevIndexes.includes(index) - ? prevIndexes.filter((i) => i !== index) - : [...prevIndexes, index], + prevIndexes.includes(index) + ? prevIndexes.filter((i) => i !== index) + : [...prevIndexes, index], ); }, []); @@ -76,11 +76,11 @@ export default function CircuitsMenu({ id: `custom-${block.id}`, // Префикс для идентификации кастомных блоков label: block.name, icon: (props) => ( - + ), customData: block, // Сохраняем полные данные блока для spawnCircuit })), @@ -89,71 +89,71 @@ export default function CircuitsMenu({ // Обработчик для создания кастомного блока const handleSpawnCustomCircuit = useCallback( - (nodeId) => { - // Ищем полные данные блока по ID - const blockId = nodeId.replace("custom-", ""); - const block = customBlocks.find((b) => b.id === blockId); + (nodeId) => { + // Ищем полные данные блока по ID + const blockId = nodeId.replace("custom-", ""); + const block = customBlocks.find((b) => b.id === blockId); - if (block) { - // Вызываем функцию spawnCircuit с полными данными схемы - spawnCircuit(block.originalSchema); - } - }, - [customBlocks, spawnCircuit], + if (block) { + // Вызываем функцию spawnCircuit с полными данными схемы + spawnCircuit(block.originalSchema); + } + }, + [customBlocks, spawnCircuit], ); return ( -
-
-
-

Menu

-
-
+
+
+
+

Menu

+
+
-
    - {menuItems.map((item, index) => ( -
  1. -
    toggleItem(index)}> - {item.header} - -
    +
      + {menuItems.map((item, index) => ( +
    1. +
      toggleItem(index)}> + {item.header} + +
      -
      -
      - {item.gates.map((node) => ( -
      onDragStart(e, node.id)} - title={node.label} - > - -
      - ))} +
      +
      + {item.gates.map((node) => ( +
      onDragStart(e, node.id)} + title={node.label} + > +
      -
      -
    2. - ))} -
    -
+ ))} +
+
+ + ))} +
+
); -} \ No newline at end of file +} diff --git a/UI/src/components/pages/mainPage/customCircuit.jsx b/UI/src/components/pages/mainPage/customCircuit.jsx index e1266239..442011e5 100644 --- a/UI/src/components/pages/mainPage/customCircuit.jsx +++ b/UI/src/components/pages/mainPage/customCircuit.jsx @@ -1,6 +1,10 @@ -import React, { createContext, useState, useContext, useEffect } from 'react'; +import React, { createContext, useState, useContext, useEffect } from "react"; import { IconCloseCross } from "../../../../assets/ui-icons.jsx"; -import {LOG_LEVELS, showToast, showToastError} from "../../codeComponents/logger.jsx"; +import { + LOG_LEVELS, + showToast, + showToastError, +} from "../../codeComponents/logger.jsx"; // Контекст для управления кастомными блоками const CustomBlocksContext = createContext(); @@ -10,33 +14,35 @@ export const CustomBlocksProvider = ({ children }) => { // Загрузка блоков из localStorage при инициализации useEffect(() => { - const blocks = JSON.parse(localStorage.getItem('customBlocks') || '[]'); + const blocks = JSON.parse(localStorage.getItem("customBlocks") || "[]"); setCustomBlocks(blocks); }, []); // Сохранение блоков в localStorage при изменении useEffect(() => { - localStorage.setItem('customBlocks', JSON.stringify(customBlocks)); + localStorage.setItem("customBlocks", JSON.stringify(customBlocks)); }, [customBlocks]); // Добавление нового блока const addBlock = (block) => { - setCustomBlocks(prev => [...prev, block]); + setCustomBlocks((prev) => [...prev, block]); }; // Удаление блока const deleteBlock = (blockId) => { - setCustomBlocks(prev => prev.filter(block => block.id !== blockId)); + setCustomBlocks((prev) => prev.filter((block) => block.id !== blockId)); }; return ( - - {children} - + deleteBlock, + }} + > + {children} + ); }; @@ -60,19 +66,23 @@ export default function CreateCustomBlock({ nodes, edges }) { const customBlock = { id: `custom_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 10)}`, name: blockName.trim(), - inputs: nodes.filter(node => - node.type === "inputNodeSwitch" || node.type === "inputNodeButton" - ).map(node => ({ - id: node.id, - name: node.name || `input_${node.id.slice(0, 4)}` - })), - outputs: nodes.filter(node => - node.type === "outputNodeLed" - ).map(node => ({ - id: node.id, - name: node.name || `output_${node.id.slice(0, 4)}` - })), - originalSchema: { nodes, edges } + inputs: nodes + .filter( + (node) => + node.type === "inputNodeSwitch" || + node.type === "inputNodeButton", + ) + .map((node) => ({ + id: node.id, + name: node.name || `input_${node.id.slice(0, 4)}`, + })), + outputs: nodes + .filter((node) => node.type === "outputNodeLed") + .map((node) => ({ + id: node.id, + name: node.name || `output_${node.id.slice(0, 4)}`, + })), + originalSchema: { nodes, edges }, }; addBlock(customBlock); @@ -81,7 +91,11 @@ export default function CreateCustomBlock({ nodes, edges }) { setError(""); setIsModalOpen(false); - showToast(`Block "${blockName}" created successfully!`, '✅', LOG_LEVELS.ERROR); + showToast( + `Block "${blockName}" created successfully!`, + "✅", + LOG_LEVELS.ERROR, + ); } catch (err) { console.error("Block creation error:", err); setError(`Error: ${err.message}`); @@ -89,51 +103,49 @@ export default function CreateCustomBlock({ nodes, edges }) { }; return ( -
- - - {isModalOpen && ( -
-
-

Create custom block

- +
+ + + {isModalOpen && ( +
+
+

Create custom block

+ + + +
+ + +
-
- - -
- - -
- setBlockName(e.target.value)} - placeholder="New custom block name" - required - /> - {error &&

{error}

} -
-
+
+ setBlockName(e.target.value)} + placeholder="New custom block name" + required + /> + {error &&

{error}

}
- )} -
+
+
+ )} +
); -} \ No newline at end of file +} From bf5151c9f0a14e9f3081fe4153b2a11b398b082a Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 18:01:15 +0300 Subject: [PATCH 131/152] inputs, outputs fix --- UI/src/components/pages/mainPage/circuitsMenu.jsx | 4 ++-- UI/src/components/utils/customBlockUtils.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/UI/src/components/pages/mainPage/circuitsMenu.jsx b/UI/src/components/pages/mainPage/circuitsMenu.jsx index 6bf51e25..2a76b0ed 100644 --- a/UI/src/components/pages/mainPage/circuitsMenu.jsx +++ b/UI/src/components/pages/mainPage/circuitsMenu.jsx @@ -10,7 +10,7 @@ import { IconInput, IconOutput, } from "../../../../assets/circuits-icons.jsx"; -import { loadCustomBlocks } from "../../utils/customBlockUtils.js"; // Путь к вашим утилитам +import { useCustomBlocks } from "./customCircuit.jsx"; // Путь к вашим утилитам const CustomBlockIcon = ({ inputs, outputs }) => { return ( @@ -36,7 +36,7 @@ export default function CircuitsMenu({ spawnCircuit, }) { const [openIndexes, setOpenIndexes] = useState([]); - const { customBlocks } = loadCustomBlocks(); // Получаем блоки из контекста + const { customBlocks } = useCustomBlocks(); // Получаем блоки из контекста const toggleItem = useCallback((index) => { setOpenIndexes((prevIndexes) => diff --git a/UI/src/components/utils/customBlockUtils.js b/UI/src/components/utils/customBlockUtils.js index 18fb90a5..964b1c64 100644 --- a/UI/src/components/utils/customBlockUtils.js +++ b/UI/src/components/utils/customBlockUtils.js @@ -5,20 +5,20 @@ export const createCustomBlock = (nodes, edges, blockName) => { if (!Array.isArray(nodes)) throw new Error("Invalid nodes: must be an array"); if (!Array.isArray(edges)) throw new Error("Invalid edges: must be an array"); - const inputNodes = nodes.filter( + const inputs = nodes.filter( (node) => node.type === "inputNodeSwitch" || node.type === "inputNodeButton", ); - const outputNodes = nodes.filter((node) => node.type === "outputNodeLed"); + const outputs = nodes.filter((node) => node.type === "outputNodeLed"); - if (inputNodes.length === 0 || outputNodes.length === 0) { + if (inputs.length === 0 || outputs.length === 0) { throw new Error( "Custom block must have at least one input and one output pin", ); } - inputNodes.forEach((node) => { + inputs.forEach((node) => { if (!node.name) { throw new Error( `Input \"${node.type.replace("inputNode", "")}\" must have a name`, @@ -26,7 +26,7 @@ export const createCustomBlock = (nodes, edges, blockName) => { } }); - outputNodes.forEach((node) => { + outputs.forEach((node) => { if (!node.name) { throw new Error( `Output \"${node.type.replace("outputNode", "")}\" must have a name`, @@ -37,8 +37,8 @@ export const createCustomBlock = (nodes, edges, blockName) => { return { id: generateId(), name: blockName, - inputNodes, - outputNodes, + inputs, + outputs, originalSchema: { nodes, edges }, }; }; From a0db906920dead37be917416bd24a2cbb448cc3e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 15:01:43 +0000 Subject: [PATCH 132/152] Automated formatting --- UI/src/components/pages/mainPage.jsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index 204cd99d..c12fb1c7 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -73,16 +73,13 @@ import { CustomBlocksProvider } from "./mainPage/customCircuit.jsx"; export const SimulateStateContext = createContext({ simulateState: "idle", - setSimulateState: () => { - }, - updateInputState: () => { - }, + setSimulateState: () => {}, + updateInputState: () => {}, }); export const NotificationsLevelContext = createContext({ logLevel: "idle", - setLogLevel: () => { - }, + setLogLevel: () => {}, }); export function useSimulateState() { @@ -198,8 +195,7 @@ export default function Main() { setActiveTabId(savedActive); return; } - } catch { - } + } catch {} } // Initial setup for new users const initial = [ From 0876a4ff404525ace4c1fe4baf8c177430634f34 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 18:55:17 +0300 Subject: [PATCH 133/152] frontend fix --- UI/src/CSS/auth.css | 3 +-- UI/src/CSS/reg.css | 25 +++++++++++++++++++++++-- UI/src/components/pages/register.jsx | 7 ++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/UI/src/CSS/auth.css b/UI/src/CSS/auth.css index 0e892568..61b8232f 100644 --- a/UI/src/CSS/auth.css +++ b/UI/src/CSS/auth.css @@ -2,13 +2,12 @@ display: flex; text-align: center; justify-content: center; - align-items: center; height: 100vh; background-color: var(--main-2); + overflow: auto; } .auth-window { - position: fixed; width: 30rem; height: 35rem; background-color: var(--main-1); diff --git a/UI/src/CSS/reg.css b/UI/src/CSS/reg.css index 8f9f7e9a..b7205a5f 100644 --- a/UI/src/CSS/reg.css +++ b/UI/src/CSS/reg.css @@ -2,13 +2,12 @@ display: flex; text-align: center; justify-content: center; - align-items: center; height: 100vh; background-color: var(--main-2); + overflow: auto; } .reg-window { - position: fixed; width: 30rem; height: 50rem; background-color: var(--main-1); @@ -147,3 +146,25 @@ font-size: 1.2rem; color: var(--main-0); } + +.login-text { + font-size: 1.1rem; + font-family: Montserrat, serif; + margin-top: 2rem; + color: var(--main-0); + text-decoration-color: var(--main-0); +} + +.login-link { + display: flex; + font-size: 0.2rem; + font-family: Montserrat, serif; + margin-left: 12.25rem; +} + +.login-link-text { + margin-left: 0.5rem; + font-size: 1rem; + font-family: Montserrat, serif; + color: var(--main-0); +} diff --git a/UI/src/components/pages/register.jsx b/UI/src/components/pages/register.jsx index 3d8489ed..07bcbdf3 100644 --- a/UI/src/components/pages/register.jsx +++ b/UI/src/components/pages/register.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback } from "react"; import "../../CSS/reg.css"; import "../../CSS/variables.css"; -import { useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; @@ -302,6 +302,11 @@ const Reg = () => { + +
Already have an account?
+ + Log In +
); From 25bfd694e573df13f2faf5f7d831e521418c9068 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sat, 19 Jul 2025 22:37:27 +0300 Subject: [PATCH 134/152] logo retrieved --- UI/assets/electronic-circuit.png | Bin 15728 -> 0 bytes UI/assets/logo.svg | 10 ++++++++++ UI/index.html | 1 + 3 files changed, 11 insertions(+) delete mode 100644 UI/assets/electronic-circuit.png create mode 100644 UI/assets/logo.svg diff --git a/UI/assets/electronic-circuit.png b/UI/assets/electronic-circuit.png deleted file mode 100644 index 2375a95f2cfa3d6c92d1ed3c32e599d78b9690a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15728 zcmd_RbyQSc-#>f+5kvt41cd<;B#E^Spn)Ydvedt~JhZ&OSTN+55Y{pEw~JY6_GT*C`+fqEvkJPz!>H z!B1j{{4Ds|_8B+?UuQgI6?MqLzX0+lZ^7T^Tpt;FKoHe8!iOlDn~DJ(yoQi7Kxn(z zAiOQzts!r3@4NQSjviK)uGV*5+-(z9?_Gx=7D(~oeI1{~m2uwy6RaivbbeyEva9Tl zVb#++#XcnE3E%014a+r&&oyVBiTH5Qj@Zx~S8V?Pvzw4ToXV-lqQRo7Odb_*DA`H&fK{>#8ZGRb7zRWa3f88~?&njmhKtOLVMKw~Aaw z^LRTp7A1MT(}O^ak`WEf8{Dn7|#4b+a%E{Ot_ z@(!+aF_jC}I(GRhx71U}bocZGxiOmR{jvimQ$+S`*gb8%C7VtsPx7F#hON3Tw!p@p zX%HlXM-L}Ru2irW;a8cPy|Stmj{K9oe?AAZCAG#kVsLe z9ZUovj~A7Fo2sPNoE6=JW=$78?#;i;en2=r`g18M2v?^Z?0nQyn)B}HmGta(Q+-1OL9fb1)3{B=;aJ2)2+|Nc`ZIAy--UOX1cL7L#c0)Xi(|8cAqe>; zBu8>6x*-+E0(#@xKXF5kbL2u_Vw5K!kgSfZ_BmjkI;Rb+=)ZI)_hM4&3s_h$Kub1ZK>;OyJp89D{wE! zzuq=TIi@)5#BcA21-+v=+}aZ~(}Lm9BS~YasO_tsS=HL)cj&x=>m0t^4D|bAc>PK) zAB;@yHKE&3tMUhPUYP!8|C$bkqcRPt!0fiIecuWIX#1cCjl*_&os$OP%mFSLfV^b8y z-%iqT3%MF)nkjlkeR1b=Thw}bw$+U`leWI*-s6{d^UW>b6iN^&M1M2o%D{2EZ#>Mf z5P3xeHjf0AZQY!A(6jqE*rsox@>*+s};;_E1$2 zziI6p)|1;HtMKaTLhW;Z+HS7|)JxWjUcra;oXkQnl3U=mGCuThEmA9PcsyV-&m00; z3@dQz!<-$vs>o0R#KQ9Y*A5LFm4#9fP0x79ay#0sB&jnf^@JQc^e z&{$9&Xl6;nL}5g?LO~Ij`R;vg`6KF~FZ`IDFhjZo)bo?&x1Mn*`qf%1&cn?=HZ z-xrH$Z7wM-EzHrJvT2FzWWpXlhMxsDi(eT@xXuTjG6-spDill^b1dcgl-O5cZHvdP zRw`{JX02U|yQT#yZ8GQ5p%w}$_T0!$UVd1{LV(L^o6a0{9Llm$!do(}>Ws2DYc&T5 zykG|BuVdp-nuy!-tqX^F^9i8HLy@eYi9<)iK0^BAE^ec$opw8=OwKjx|equgdSLj=rfQiBC9he=~m2UV4>LpyS@X-Y~ADj zYuQ#Z5$x)sD?SE8jcz$!f_xWQWr$=H6RyI)BVI<_C!#lHTJ%%3ZQSQQ+^w3-=N@)} zQ-yw*L_jR&i^q{VOD+Pij3+EBAo-b$?(>yiFj9+(4!>lx<mdv13_O!8ea%P zGNl<1L|=uYO(cRsVW6QBn691+oEUj2op78h=o{k9zpjlYqJT7(!4CKu{e@WsM$|$M zR@(&zQi$W3-c{uL7;p%JG79fNoZmqkp(8T!f#e)s;1vD`6Xp^!T^_WM5>1*aScyfP zNzL8Kd7RA(y2*XM#kd1ZQb-QXs|t!F{Z&MlzjzXHA=S=_c}C#6#xkfixy(qqrvI;F z*X_W%X(2Z>gxKr@1ESN12?Lh1$H=_FkwfzBBg7vzwEQfmh!m4?95Q{wjI$R&#|wF& zhziMr(7|s^VDWHpk>F~o9*UDdG!g~pk!+2Z12sl3^TwfwXu@oFoprbpzzhRTm_zq` z{nAg>8sYo?ZsL$wLr&d<5aGTXm_rkDIP7O@Bdm9Q`mi&`5~;ZoQX~oYuj&`AN>xk5tu3$8N8*- zY^RxJT%3C4WAE=vA;a z`p=+BEl5exszhE|>M-)qrt6kFtPR0PPgr!y{)&y){J$XHr|(7bgxj-#kVxX|I{Q+t z&UcQTgXk)#Yz7@(tR>KOxTBCSO^b0FHIOWiYAk;Cyh-dp7Q?S$0akd!6OnAuKj8@| z%?jt?I+I4+eGLBx+aY0IYZat5At=TFV!htJEDYjSMeFY391k^!u*9x23(VR0=$p3Lvx* zr$Q}UeNU^C7n_amoCLTg-#h7c^v>hy>5Q#{S34(zkPcBmg&?n1D{j+^Me#d~R~%SM z4&d`~^n42-s(^qFfid-W1vl#|IFy53p-0ePbisV$xDu3j>i*Z>MW`ZbN5H7>RNFJh zI}Rn_&@eoi9p1cdm1W4;d?_V{xJiBbB6TUrBs81HeTg=9OezQVqw}uIK^fAKBt!RA@EP)l(VF?@7$-0OW zzpS7)Ao~EfXr0N>dS`J;fM@N5`g_?)_VkR(7{gDiK^OJc49VsOCwrU{3uVmgF8_$8qq@hEE51sa^D+OzN5K)OW9z$0gl=*bs ziYnRtx6SKQKr{i<2coj%8_l3SsdkW?R1bA;LgH0o^tMkPav!>2EvniOiHV9&v=8Ue z&3F5{glv2^5z}*IG`(fVn;} z7rl=WfU)xuwk~P=K$Rg8USU`k+gSf$F4OQ)JQyzv1;qF88gG_9S&Y5u(^uKA=p3HS zn8=NBVM-@%{g&I2Up|KpM{|wbfA)3b1lA?Qc~5Vx03@eMn*Of6U5CF>Z$K9CToY_7 zO1^H5myEMiH?Z-S;HsFf=XJrJ7@sFGo(&F-#2TP0V)){-K9nWr@HY5XSA84Vd8Iht z_0bBhrMWSK8yuuPTt!XtulIs|Xo@_m@g>M^9RdTgK*lXBjEKm7rN;To@cBzREb1Hg zNaB7lT-%tUth=v0GAF_7ULj@Q_~Fr~cfwLSj-Wm}WyDfS)xFd=weyokG!P4!Uw{3M z)}g2$=OKgcVXH%c#i#feg4h{pqx+(w+yz;UnQ)A8r9|z0r##V0-tkVBnv;72JMns} z#>MJFJpI@!J^jBdvT%Rr(V zTtWXJqoGu#Q!!H7h{TU8 z)3`hww2PyvJ=)!s2_a?O8 zNAZ>C!RZi9?0?}f*e>Ve{|kOi9ISIFcKRrKHh6^Aur|t>;#O#gp%x-dh+?KU_}f*k zKXdaVnZ4)oIr)@TpBqNdvz>*+#3*E#NE1tJUaDiZf68L_+voz0ZseE%WaTKokJ!l^ zt>DKOirh{vT@>MmTELvzWSM#`%w6%wqpL@=ol_L15TP^e(yvGxV|Ra8&QEY7(~LPl z0pWvY4^NTfu zo4+HX?zss`lo@#9LyY>o3uu))iIHJqBzzaFohy5ao0?uyy zG@8)s+>xQwcMeVIMyF>mniScs(q?M~2l*S7t3*((0ulX->*g7Ea}Odq$N94}XiUDL z2Ys3oW;=9+cVr-qd~ot2;--<2Cl8ra zqxQ@EBA&c@CaA@R1{v%MOX(?U8q9aZztNs%F21Fk@^NWjMS%!kdoInEM%k;fElQbo+$6SL@m8Jd=&Ef_HMv zEmVYt=lwwQ!_ELC&H=5tDScO)mMw z4Q7x=CMdivem_a)830Lg^HHH)Gu|&iQV6{cAznuveB9C@JpGJ?sCp|Q(Xs(TzxXtv z?OlqXa@KdMZ5&1(UzBMO0E2*xmV{KMzwhIB3NcFzf2bY+)C=u#e^vGQka+4JxRC%8 ze|MP_lKXZ@%qJM|8hid^MyZ{1X!~d0UuE0DVID*%j{KIDd@SILzJFOH)|gMpGzyBu z(cj7d56ddxiYlSG=ck>e)v-Ou%L2jy5xa8U@hCR~lu6k63Og;=rdCoTo!<-vA10b2 zZ=~I)d{Q?MHrsF+s#(N+N5A?T3lAIVPb%;E zW@j*d=sjoWwST9ZQ{pJqO8REL&NuTszf+p-kVJIsP_bt)_qP18n{|WE^DE)bQf=6Q zb}^VaHXY+=yZAodFn^ociO;Tbve+SLM%|p*TzJ-WGTb`Suhu&ud38LO&cAvQ`y56? zOqhl3d2fxgj3ythbXCNoHtA3lOgWKUcdzZ=V|v{L0g5Z+<){IJMa={4kB87iIfx`5$N@2IDaD?b&c6JDtt)P=#H3P9^$CHhY>;W^eQ#rJ zTYp+j07k|^K?BXmMLSkO`-w=k!u^7KF5mk)Xk$9{WxH;KQ)3rV;wgPA<;8>FTsP3D zVfw!)XiX&g$iTLkZT;5wj5Qm2&0#PG$Rn|zr2K5QH zr^|CquniG)Nuu7_x#J!ki#fQx=Oam5J{AmHD^pZ8ihnGhrP(N&@h}as^t2Ir*DdINYc639X6Fr&bX^mS%kA zaBO`zd(69Vs7TlDi8{X8(bxGggPZ+Me^77ps%az@Q7Xiu{sL7iI@*zT@M_YgWPOw| zuS7e`e=f+3U5D1z_ZsR-fe~VoUzG1I^otq{>yqnh5!h3yfg@vz=5Cqa)#8_F^M`Iu zddBNI_dYhjbY0(BvTDkWyDDirr9l`|v*YR@|CJbKuJOYcE?bXu9TI!i%?sWx9|?_J z%pa88!pm#3&berls$>9yomjy8Y|FPQRkKZc)Z6^ZpQoIpy71tW*99UPMdm-6)l^NW zkh1{PiA#~NHmN@Ks3E3UqM2m>i-`*Re0^MaFJ4oylHYU_>@yu zd>W+X!r3vxi&f(KU2BIRhQ9}6=j|VUi6j5j`*fZdYhOly79qMkhn+L;Kxq8yycUx! zB>(QL?WgQoTS7b!wE1(H2c2LGKda$Nm?Zu0;!u#{CG3LOq{6M}2{!54Kka?q+2%#9 z7GVI5-`9jb(4E8qFu-EpwDy1^^ttmJZ3d2^>g>Q@0Nba6JM?Zs1>pEWhP5BM8G@q# z7?xHzC-)R3ID#hlKog{-Eq790#4k1Vf^%EwJZ8PP-Z3^knHz+v1BcZxmkLG3C6%pv z=t2Be)6UgPNiwL3W+RFuELcJ^_L|s2Ny9pRG1(cQxWgxy$9hyRX$dEH2W+ex-{a^9 z9o*C-%@bRtw7^m*vNL)DKl{NOv~1$?wp?fQF>Nn7|M|~uN zibBbt<|?Ni?{*}&FkT?A!YvI)FJlJIY6lOSdD?mbwhV1Ee+EFIR%-xT#V8+k=(7VTMhNI)I$*45y~1LvcM8DoMqLYsDvZ;H@uS* zT(-l|YitN`4x6oug3#Z{9x!hLAPL4M&22ZILz$?$639ZCOQm(tqyFsu@@&?q_hB2h zpsRY7g7ZEVmuz&9ng`h@a7!`I)ME0;c_-h1J=`fRymcCd_= zj3wq0M010pg=kr&2(wg5n{D;p$kD0`8&81z6TiI;s+n5WL2~nF_VZ*uYvnNFy=D$; zrDE|z0&9M>5kd@E>~lb5AO8m7SF4BB)%~jVc286ctpOALES3an`IrhZ%7UAd5m?P9 zjm-S~H2l-vNi0zWq@>ml;J3vY(kZ^j!)82d_1_msga1q*nQ$lGa^*)V3ZIEGzu&vi z@@PA2Ik6*(nD0zu9F^q276~^svh-;&6Ryz^9KdM0Uc7b-3^a6P*Cz%?F0TQyMj*^s zOHP(MsK|isJR90F^~?c~`}2)VNdONo{{_51lT)xD>XlmM72-d^K_eI;+kLEkwF3e8|9**ybSlQey^ZaM?JC4On|EFq8n3qaFc_=>Dn za97EJLNQJf`W#Eqf8Nx7oxadQUXe915G;9Wbh-Rbha0BDf5?yjbPOy(+855 z`+`=UwIVK9f$Ui}c$f*j(5%}XQVXv=7SP>aR8hG82_SC##l~>mlh9wt-B=Vv15;0S z9y><NO?kzG z*h?4b%Z!bfv8?{)V8(I)SOU%y2Ig5x8-LX-_u!)kts9`u{bB%?WhiB@kg5v3|LoS6 zErF3GCybbFHVBBCKL22AJ{0i>GEUwi(Z>Z9oJ~f8vOtFNNpm<+l@|ohh;WGENfKBVelHQv69#P3(XKc4y}u?CAIZk!DNI;Uas%lFUC+dF~u3#kQV|wiBrvt_&2!=VMgC#-ZqIzmxrVM zr1Y)lhjRR44m8vMs`xlzg1*G|g!>Q*W1) z5FQ)rQvd_h&J8$cSudYn?z~)(*cVH{u#GSfI7JAD`;{CVJGX9+LJ;F`f)~IG&|h+E z(%w>OcCQZvG)4ei4;fyR%!;BD1+jrLbNbMCG!kUzz5uQ$sdT<`OxUXcS1j$Ch8OT& z5d_gz0#y4##75u#=cp~%8-}BQ<&1h2fjG=TPB)U&k;&rN?_FQN+y%`0 z!%Ewsym4a}aD@+I?K*m_x(94!7NGfa#*qijN}<*%)HxEgSW(f_Vz3>6Ef9J6s&=?p z&#{(EG5i$?phO%KRyF<0+7L8^9Z)7@X?kyY|&Yzb2yCsAm|mkWB?HZ+zFCJ2rL2}H6M0g{o7 zoPKFNm0cJlak;TBU?s>-xe$Y(X#sneMHIQ>cGnT|jsEw(O)HMssuZV4}NjyE3670fTQz0dmE=olG*%gLr1#Ts4!{Aa`TO zi+jFxjzf`$`%BV#F7dQ@*r#kyqbH}G8{fNxO1vR;y*|6rH z`GJLRAVzXq2znHejGIW^Yu;>(h^2Kon5e8e{N_IyZ-^e}{hIFSx-J`8T>ox~UoBeOpjOT!KbK}|% zHqW-AHo`|ZqfnDBTtYKOC{d@RoOm3U%e3mJN{whprQ?aMhuTF6QqQQ^ucEc5K^l|8 z_|)64+7>I~ufZF=vek-{^0>^|*hlGqux%?%dP$DgYxW{se%3AYL?jkKe6PSZ^>8B8 z8;9P#JC12G`WwwvK5VsD8XgT|qP>>mE}H8e^hg&k5PkF_BGc}6Uek7JI__oq@XM<2 zyLQHf+M{PBDuoC0cuLhiEs}1&C5BjR1n7{E&L@<5u^V*9C=Iq_V&YBajh|Bd*cS}=8=;`e2!CP#Bc@9POf9V2HxZl z`f0Emvk|Q=#3`VMZF+lL)Qb?B;?;mC-V!7Gvg_FoV}fvx)c2f!&JRtZlLv^J!kdAl z{-1^g`paN1gygTO!y6s4&CK?B>m2tY4+pc!RM@X$fh{1_r6S-F$l@`YNK4BL5R%0G zo`Sw`a-M?vvGIZF1lL5@#|(zsDaFj)hiAxwXm0H~KyQR3$8v4e@Cr&T&w#%m+$KdnQi`rMNCZLE2c7>L8pZ;0ZoOm@&S9YCBgTMoltT&*ZUsC-( zfGZ)b2AMvNTt=dN5P#38A4oFJBw2+i- z$~!RVBDWu{ z|1+e$P3Z}QWc-sW(CM=YxS@2ASz$Ca*#4f)?HQcvra;qGq5^Zn>1KT?`cljpc*K6T`zvTY$VgPZO<%1qv)w@V=w?Lbgo=vG_qy%J zry@LSMj0_j)!ft8l#>w*TSkF~KhiFvnA`zY!@-jEz*Z+_8KCLJi*GkNnB z{`lm5jE;g|ffu`I?thWtsZ+1rE#{|!*uFSx=&KrJs;i4BTM^Te9gBK8zXJNGk%nhf zQL^i&-R!g53?al z3!UfQZKHXp7t)G`Hls@R#Y8L3mg1;1^Z&!!6lQ}{thTp0HUv^_20^bOt@Q-<1k`@% zxMUyev*oCqBI!SESA*TnAH|3!=PNU)^BV!Z-3n3t47-uIYHyDI{N6O>F-*Mf*{1K| zm&~o%nW-NgruE4K(oX&|hT#vGNm;1>A8x3LThU=l-QObp0@wL(HLwP4wJqEyYDZH9 z889;k)k#TgJjL#JQJo@*uRm(rc{i@UG?>Ia5XmG2V_*BXTj~QQ<)~h=c;S%J+t{4A z=FEo&z?1?gUIK$o#QqfCLqs}OGAGxAE&O>CWW)XOzcM;~yaLl9Dz^XN&E5nd3EW(f zy_5o>_0^~TvIO0%^m0?tNp#0r*THA=hzb2>nbB$g?UR)*cYOJ~bEGT^fOtl@A7uSy zaS>o65-qgqy!&%|UAB?t1YO0JEDAfvH@93@TUR<;EW~fZc;o`Oxn6a+dW=YN;hM8? zHW~cX0hz5Cy%iGZtu-;h4L`Qh%B~HMS$6V|NKoOnuV zDvcwqRl}_`qZm!aEkEM@@t*m+)J`u-`{rKee+pLHjHI>-jyBTp)%jc{8V@jU+TXsh zg3ptW;Yt(2XR>i$7J2i=x=-!JQZKfW79e_$dzW4UVLfAJ9MD~LYq1Oz?$;%Uz#h-N zFVZVq(%pw7=6m?xjAef2-TDOO6Glq^G4G?>;ZCV}f1$hovzQz}s|~dUXbQet7H2m1 z)cIWPn@F0qF2eym(3o}{AQd*(s+DL3Df7D;EWG&8Pxt6L;NMW~6=i3~y?;c6H@N;7ekGK%fh?6QNHqd z|FvaNlgj7rCy()-a@t6)lUFGZ(Oepd)3@Y9iCxL>v!`e24!KWZB;;}zM`qEwP3se>+;|>a6MVzqo44wFz(ueCGiZ51aphwC>F!rr|_saE5x1 z>wiS1#On2c zdxy>~m=KbIIt8DMKeUWKkj=INnq`Xw5G=J{754zvQhOEEH`cC&;kT%DWarWKh&}PH ziI=TBTz+`=-e@RsYPJ4;0?hC!0F?1p%)WF!=gV$2&n}4X5dt_iu*}}AuZZ#O$28~> zoM4d4S;we<3Pmyu_FbAlTDC@Cxtf=V`K3_!g?W%x74Wu5qCq;S`N}{cdGj%=u!Q^7 zTNI@FbLVlP8_UhCJ00iVacW*IYl41UNU_|Jm9D+)-YG1J0A7=lAE!e;`F{ypVBs-4 znSjAj{P+)D3;6{IF^U96v5Ny51PtTA0ejwH@8cV_T0k7+Zwdh;zr4j2FC6*(nY+zj ztY-Osoa(X($p#D~8{pSoNj-o^8EgaKuDLI|Ix6N8Rc+U7M;>P^N=1XVw7sB?Z zJAF7Ymu`S;{jjBG8juX)%!SfslM}lP){?!z?t`q1ffkRuL&c)Aty0M!CMqRq;BS~} z`DdUg?gBG$#GHvpK)ma8@OYhM zB={Bh20b0R5tvIFEQ42(t&j_gm;jKjDswlvl#ut&7l=Qa?c$_?91RR1u!vr_=m6u= zx9pdwp;GHLz#S<)8_KzopA9BbnOafu?dB!GMvz)q6Xos-m%M<e^uY_)yxPaTy(nCBY5{p7cOfAfWD$i!9T-8y28LZt@`Oqi4;&~EjOE0ge zJ8;hS^;uq(<;u)sOx!HWdLLm;!hK#QI$ZSPOiyC_u>0~@^m{u_1_Gn(7_xqSw58J< ztps?CGf^Mj8s{BC!Tl6j;y>^SE|FiXR25{1z&Kv00dHVbEXYH%Sq(Be+rY_asZYCA z&!b6MrZ&66#z1-+i?RrFm-dEcj9;bfJz@Y?9aFimw4;?`?rXo&>dC=06if+My+|UJ z0rdLSm}rWeu{NiegI2?<5}+*7WJ`STZsEDqN5JaWuLRh-DRMbMML-Xk33dBtQZCHA-$3^ zXxnx#o={u}K43Yx%=azLQ0dG=Rec~r1lvd97It&8LPpPG;hv;5nyv+GS6{ zvuyXf=9yUq5Y-fUX8E_T7pT>Gbf|2I{0KrWRMP75i^ib$`1POw`9c~y=fpz^9o3? z3#(N*gkz_*Tj%5if1n%~mVk}(Z>Qk(X$3BjXi_TCTsY{h(p^;7mI@z`ffoTQ4?uGl zQlUpmHdcPD_!>avIhqE%ieMt8u-VR;t}6&5_RDmh+^IPfSZ)>p(u0-i@+HbAyYV2v z1DB~0gZ~f21%uT#bapW#i z)@jAg5J9^L`W4Xp*l^d}7;G+>c!P|=86zDw0V&1as75P?)&}izZ49l_rVd*UV%h6-d}7yFSn`oAV{?hX-R|Jduvm_ zqjNeQ5lekEbx%@oee(K`-B>E(hOX6z)lCh*={A1_QFlh>6g$-^tSIi9j}q(|w(d!0 z;rx7a7K5#y3WQ4mfj))QTQM$9Spo33HFpOyNbC2MHwJ#6NGvyoET{lAV1wb8PeKjc z2EN&!p!A2PdaF3Ig;57kdMjYfCNQW?0K?CxqybUgT(Yi6Kh3%0T%H2Fe(2?+xU$Z8 z6fvU~JfSaVaJ=)6$k8yMt{ev^)84n?-&KSD+=vUq^)(sCGRyQ^R4H^FJ^KME(uCm= zCi>`RU5PkmnUxME)15<=_C|ycM1(7iz~ec|}H(wV)mr-!(sC9=W_kXbGrI zZ$TNO!7a&x)-&6g-b>z+le*q@-3S_shgIL;o*M@Z}h9#v*BK)T6pBtzvds5YB$)VuF9dB)FEZsG^YS`mUVFBOb z@YsKQ8qL!q!+idcR>FxMaPR=P1L4BD1N57y9VM63of%w_sruq{@u@+hz25%UG{Bv5 z$p7tc1p6t$%|uHuA$NwpWil8O{Xn=4jC%89_$S2(M9Dddw}v)1a}% zI?we-FAlYH7bsYwkJar0pV9^&BTv4SD-!O<$*A-4Pmd^tCh$;hmdG{OydmI*b$HP*?~S$T1m)$i6%AiVNVX zg^68TPlBDK;`fv8JEHZc&Tp%5`YXLlF)-jM`F%y&J(&1|0x%2`?3~QzdDl;UR4J1( z?bEa=y!2T9g9~`^^a=-Ua9rlyf${Qn=(wblOO}a}7u7miXy&_`Q3=gZ_!*kM2W081G6DsgtXRdg}T=<5x zD8G>9;933Kl+cN}Mys$Uo^hAkYC znNqI5o=d^Ic>1}%o(WJz!8hqHLC}nnTuTR&`8a#h# + + + + + + diff --git a/UI/index.html b/UI/index.html index 5b5fc810..bcb23b64 100644 --- a/UI/index.html +++ b/UI/index.html @@ -4,6 +4,7 @@ + Date: Sat, 19 Jul 2025 22:52:28 +0300 Subject: [PATCH 135/152] successful build with logo --- UI/vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/UI/vite.config.ts b/UI/vite.config.ts index ba3d0846..f4533cd9 100644 --- a/UI/vite.config.ts +++ b/UI/vite.config.ts @@ -4,5 +4,6 @@ import svgr from "vite-plugin-svgr"; // https://vitejs.dev/config/ export default defineConfig({ + base: './', plugins: [react(), svgr()], }); From 725f2849a9bc12e1d7eed27639eb9d18beb2ec96 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 18:04:34 +0000 Subject: [PATCH 136/152] Automated formatting --- UI/vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/vite.config.ts b/UI/vite.config.ts index f4533cd9..86c5754b 100644 --- a/UI/vite.config.ts +++ b/UI/vite.config.ts @@ -4,6 +4,6 @@ import svgr from "vite-plugin-svgr"; // https://vitejs.dev/config/ export default defineConfig({ - base: './', + base: "./", plugins: [react(), svgr()], }); From eeeac5d665166c420bcbab215f1e01711f095fb4 Mon Sep 17 00:00:00 2001 From: witch2256 Date: Sun, 20 Jul 2025 21:06:51 +0300 Subject: [PATCH 137/152] transition-report.md added --- docs/reports/transition-report.md | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/reports/transition-report.md diff --git a/docs/reports/transition-report.md b/docs/reports/transition-report.md new file mode 100644 index 00000000..00552d57 --- /dev/null +++ b/docs/reports/transition-report.md @@ -0,0 +1,89 @@ +**Заказчик(Михаил Кусков)**: В смысле с продуктом надо как-то взаимодействовать? Или просто общий вопрос? + +**Матвей Канцеров**: Просто вопросы. Первый вопрос. Готов ли продукт к использованию? Что готово, а что нет? + +**Заказчик(Михаил Кусков)**: В смысле, что мы подразумеваем под продуктом? + +**Матвей Канцеров**: Visual Circuit Designer. + +**Заказчик(Михаил Кусков)**: Сайт, насколько я в последний раз его помню и видел, он работает, запускается. Сайт можно открыть, базовую схему можно собрать. + +**Матвей Канцеров**: Так, а что готово, что нет? Из того, что хотелось бы видеть в продукте. Ну, как заказчику. + +**Заказчик(Михаил Кусков)**: Ну и с самого базового функционала, насколько я помню, готово всё. Последний раз то, что мы обсуждали на прошлой неделе, там есть ряд багов, которые необходимо было пофиксить. Из продвинутого функционала, опять же, то, что мы обсуждали на прошлой неделе, который уже выходит за версию базового MVP, это вот потактовая симуляция с введением клока и, собственно говоря, потактовым симулированием работы схемы. + +**Матвей Канцеров**: Используете ли вы продукт в целом? + +**Заказчик(Михаил Кусков)**: Вечерами? Ну, да. Бывает, ну да, да. + +**Матвей Канцеров**: Как часто? + +**Заказчик(Михаил Кусков)**: Два раза в неделю, допустим, примерно. + +**Матвей Канцеров**: Вот этот вопрос я вообще не понимаю, но он есть. Разворачивали ли вы продукт на своей стороне? + +**Заказчик(Михаил Кусков)**: Ну, на текущий момент нет, потому что у меня, собственно говоря, нет представления, где находятся исходники проекта. Но в будущем такие планы есть. + +**Матвей Канцеров**: Исходники проекта? + +**Заказчик(Михаил Кусков)**: Они где-то должны быть, я так предполагаю. + +**Матвей Канцеров**: На гитхабе. + +**Заказчик(Михаил Кусков)**: Вот. Но я не помню, не помню, смотрел я в них или нет. + +**Матвей Канцеров**: Хорошо. Что нужно вообще для того, чтобы продукт вам перешел полностью? + +**Заказчик(Михаил Кусков)**: Прислать исходники. Ну и обсудить инструкции, детали деплоя. + +**Матвей Канцеров**: Понятно. Как повысить шанс того, что продукт будет использоваться после окончательной версии? + +**Заказчик(Михаил Кусков)**: Если мы говорим про окончательную версию в рамках курса, то самый главный фактор на мое усмотрение – это не потерять мотивацию, продолжать его развивать. Если у вас есть заинтересованность и мотивация продолжать его развивать, то будем его продолжать развивать, потому что у меня уже есть некоторые соображения по тому, как его интегрировать и в университетские курсы, и за университетскими курсами, и предложить в том числе, по крайней мере, узнать интерес нескольких коммерческих компаний, которые однозначно могут быть. Если им это интересно, то они будут заинтересованы в развитии проекта за пределами летнего курса. Несколько университетов, несколько компаний, которые так или иначе могут быть связаны с образованием. + +**Матвей Канцеров**: Ну, в целом понятно. Ну а теперь нам нужно показать README.md наш. + +**Заказчик(Михаил Кусков)**: С гитхаба? + +**Альберт Хечоян**: Какая ветка? + +**Матвей Канцеров**: Желательно его посмотреть и оценить, всё ли понятно и что можно было бы улучшить. + +**Заказчик(Михаил Кусков)**: Оценить с какой точки зрения? С точки зрения README.md? + +**Матвей Канцеров**: Да. + +**Заказчик(Михаил Кусков)**: Ну, смотрите. Во-первых... Во-первых, README есть. Во-вторых, ссылки рабочие. Это уже плюс. Так. Далее. Общее описание. Окей. Детали. Контекстная диаграмма. Контекстная диаграмма есть. Она о чем-то говорит. Дальше можно подумать о чем конкретно она говорит. Основные компоненты. Roadmap. Так, инструкции использования. Основная функциональность... Угу... Установка для Линукса... Установка питана... Так, вопрос. Вот эти вот деплой инструкции, которые есть в Readme, они рабочие? + +**Матвей Канцеров**: Да. + +**Заказчик(Михаил Кусков)**: Хорошо. И документация. А, ну вот диаграмма, которую мы обсуждали по поводу микроархитектуры, фронтенд, API gateway, постгрэс, auth Микросервис, токены, пользователи. Нормально. Ну и тут уже детальное описание каждого этого. Вот эту всю документацию вы оформляли в рамках курса, правильно? + +**Матвей Канцеров**: Да. + +**Заказчик(Михаил Кусков)**: А диаграммы где составляли, в какой программе? + +**Матвей Канцеров**: PlantUML. + +**Заказчик(Михаил Кусков)**: Окей. + +**Матвей Канцеров**: Хорошо. + +**Заказчик(Михаил Кусков)**: Окей. Ну вот, единственное, что, наверное, Ну это такая минорная вещь, но вот здесь вот в плане описания backend технологий, не знаю, там на плюсах тут пользуются какие-нибудь фреймворки? + +**Матвей Канцеров**: Да, используются. + +**Заказчик(Михаил Кусков)**: Вот их тут можно указать просто для консистентности. Это не критично, но вот единственное, что я могу сказать. Все остальное более-менее понятно. Если по инструкциям работает, то вообще замечательно. + +**Матвей Канцеров**: Ну вот у нас как раз есть вопрос, сможете ли вы по инструкциям в файле установить продукт? + +**Заказчик(Михаил Кусков)**: Ну, наверное, могу. Никогда не пробовал. По тем инструкциям, что я видел, должен смочь. На глаз. + +**Матвей Канцеров**: Ну и какие-то две секции, ну либо раздела, еще добавить в README? + +**Заказчик(Михаил Кусков)**: В рядме Contribution'а? Тут есть где-то? + +Это как раз L.A. + +**Заказчик(Михаил Кусков)**: Development, это как раз Contribution файл, просто его просили так как назвать. Вот называется Contribution. Угу. Так это что такое? Это требования? А, это колонки, описание колонок. Интересный у вас курс, конечно. Нет, про contribution я имел в виду более-менее кто участвовал в этом проекте и чем занимался. В том плане, что development – это development, а обычно в документе contributing там обычно расписывают инструкции по… Как внести свой вклад. Да. Я не уверен в том, что… У вас в этом документе вот это вот, это действительно больше описание самого development. Хотя... Ну ладно, тут описание, как ишью создавать. Ну, конечно... Ну ладно, нормально. Ну то есть я не уверен, что если человек со стороны придёт и захочет что-то внести своё, вот по этому документу он разберётся, что им конкретно делать. Но тут написано хоть что-то близкое по смыслу, поэтому ладно. Кстати, вот contributions, не contributing, contributions, вот именно список авторов и лицензии тут нет. Лицензия есть в MIT. Ну, ладно. Ладно. Закройте курс, получите оценки. Ведомости коллеги отправят, потом договорим насчет лицензии. Окей. Ещё какие-то вопросы остаются? + +**Матвей Канцеров**: Нет. Всё, спасибо. \ No newline at end of file From 95dda577a3829d3fc0ef48a96849852dc237d975 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 21:24:36 +0300 Subject: [PATCH 138/152] nodes automatically appear in circuits menu --- UI/src/components/pages/mainPage/createCustomBlockModal.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/UI/src/components/pages/mainPage/createCustomBlockModal.jsx b/UI/src/components/pages/mainPage/createCustomBlockModal.jsx index f0eed2c5..11926dfc 100644 --- a/UI/src/components/pages/mainPage/createCustomBlockModal.jsx +++ b/UI/src/components/pages/mainPage/createCustomBlockModal.jsx @@ -5,6 +5,7 @@ import { createCustomBlock, saveCustomBlock, } from "../../utils/customBlockUtils"; +import { useCustomBlocks } from "./customCircuit.jsx"; export default function CreateCustomBlockModal({ isOpen, @@ -16,6 +17,7 @@ export default function CreateCustomBlockModal({ }) { const [blockName, setBlockName] = useState(""); const [error, setError] = useState(""); + const { addBlock } = useCustomBlocks(); const handleCreateCustomBlock = () => { if (!blockName.trim()) { @@ -26,6 +28,8 @@ export default function CreateCustomBlockModal({ const customBlock = createCustomBlock(nodes, edges, blockName.trim()); saveCustomBlock(customBlock); + addBlock(customBlock); + setBlockName(""); setError(""); onClose(); From f1ccf66e107da76c22e31d6aa18e7ac98fa842cb Mon Sep 17 00:00:00 2001 From: witch2256 Date: Sun, 20 Jul 2025 21:40:25 +0300 Subject: [PATCH 139/152] ai-usage.md added --- docs/reports/ai-usage.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/reports/ai-usage.md diff --git a/docs/reports/ai-usage.md b/docs/reports/ai-usage.md new file mode 100644 index 00000000..d2223244 --- /dev/null +++ b/docs/reports/ai-usage.md @@ -0,0 +1,22 @@ +# AI Usage for this project + +## Week 1 +We used ChatGPT to reformulate questions by the Mom test and to review technologies and libraries applicable to the project. + +## Week 2 +We asked ChatGPT about Socket.io(JS library), about FastAPI(Python framework), and how to work with Radix(React library). + +## Week 3 +We used to add theme support, a file with color variables for all color themes was created. While the color scheme was hand-picked for the light and dark themes, the rest of the themes were generated using AI based on the first two themes. + +## Week 4 +We did not use AI this week. + +## Week 5 +We used AI for creating a template for bugs, because we never created them, and template for pull requests, because connecting them to issues with the github interface was more convenient for us. + +## Week 6 +We did not use AI this week. + +## Week 7 +We used AI for accelerated learning of technologies (sqlalchemy), for understanding best practices of writing different architectures. \ No newline at end of file From 253be219a2bd100e0754e584db76f5d2c7f32c56 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 22:23:54 +0300 Subject: [PATCH 140/152] custom nodes created --- UI/src/CSS/customBlock.css | 44 +++++++ UI/src/components/circuits/customBlock.jsx | 0 .../components/circuits/customBlockNode.jsx | 43 +++++++ UI/src/components/codeComponents/nodes.js | 2 + UI/src/components/pages/mainPage.jsx | 15 +-- .../pages/mainPage/FlowWithCustomNodes.jsx | 35 ++++++ .../pages/mainPage/circuitsMenu.jsx | 37 ++++-- .../pages/mainPage/createCustomBlockModal.jsx | 5 +- .../pages/mainPage/customCircuit.jsx | 111 +----------------- UI/src/components/utils/customBlockUtils.js | 12 +- UI/src/components/utils/onDrop.js | 45 +++++-- UI/src/components/utils/spawnCircuit.js | 49 ++++++-- 12 files changed, 248 insertions(+), 150 deletions(-) delete mode 100644 UI/src/components/circuits/customBlock.jsx create mode 100644 UI/src/components/circuits/customBlockNode.jsx create mode 100644 UI/src/components/pages/mainPage/FlowWithCustomNodes.jsx diff --git a/UI/src/CSS/customBlock.css b/UI/src/CSS/customBlock.css index 937a222f..6338f1cb 100644 --- a/UI/src/CSS/customBlock.css +++ b/UI/src/CSS/customBlock.css @@ -121,3 +121,47 @@ font-size: 14px; margin-top: 5px; } + +.custom-block-node { + background-color: var(--main-2); + border: 1px solid var(--main-4); + border-radius: 4px; + padding: 10px; + min-width: 150px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +.custom-block-header { + text-align: center; + font-weight: bold; + margin-bottom: 8px; + border-bottom: 1px solid var(--main-4); + padding-bottom: 5px; +} + +.custom-block-body { + display: flex; + justify-content: space-between; +} + +.inputs, .outputs { + display: flex; + flex-direction: column; + gap: 8px; +} + +.input-group, .output-group { + position: relative; + display: flex; + align-items: center; +} + +.input-label { + margin-right: 5px; + font-size: 0.8rem; +} + +.output-label { + margin-left: 5px; + font-size: 0.8rem; +} \ No newline at end of file diff --git a/UI/src/components/circuits/customBlock.jsx b/UI/src/components/circuits/customBlock.jsx deleted file mode 100644 index e69de29b..00000000 diff --git a/UI/src/components/circuits/customBlockNode.jsx b/UI/src/components/circuits/customBlockNode.jsx new file mode 100644 index 00000000..f222c0ef --- /dev/null +++ b/UI/src/components/circuits/customBlockNode.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Handle, Position } from '@xyflow/react'; + +const CustomBlockNode = ({ data }) => { + return ( +
+
{data.label}
+
+ {/* Input Handles */} +
+ {data.inputs?.map((input, idx) => ( +
+
{input.name}
+ +
+ ))} +
+ + {/* Output Handles */} +
+ {data.outputs?.map((output, idx) => ( +
+
{output.name}
+ +
+ ))} +
+
+
+ ); +}; + +export default CustomBlockNode; \ No newline at end of file diff --git a/UI/src/components/codeComponents/nodes.js b/UI/src/components/codeComponents/nodes.js index 1b7500cb..f4dac344 100644 --- a/UI/src/components/codeComponents/nodes.js +++ b/UI/src/components/codeComponents/nodes.js @@ -8,6 +8,7 @@ import InputNodeSwitch from "../circuits/IOelemnts/switch.jsx"; import InputNodeButton from "../circuits/IOelemnts/button.jsx"; import OutputNodeLed from "../circuits/IOelemnts/led.jsx"; import SwitchNode from "../circuits/IOelemnts/switch.jsx"; +import CustomBlockNode from '../circuits/CustomBlockNode.jsx'; export const nodeTypes = { andNode: AndNode, @@ -20,4 +21,5 @@ export const nodeTypes = { inputNodeButton: InputNodeButton, outputNodeLed: OutputNodeLed, switchNode: SwitchNode, + customBlock: CustomBlockNode, }; diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index c12fb1c7..f0463c03 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -70,6 +70,7 @@ import { getEditableNode } from "../utils/getEditableNode.js"; import { handleNameChange } from "../utils/handleNameChange.js"; import CreateCustomBlockModal from "./mainPage/CreateCustomBlockModal.jsx"; import { CustomBlocksProvider } from "./mainPage/customCircuit.jsx"; +import FlowWithCustomNodes from './mainPage/FlowWithCustomNodes.jsx'; export const SimulateStateContext = createContext({ simulateState: "idle", @@ -162,16 +163,11 @@ export default function Main() { [setNodesCustom, setEdgesCustom], ); - const handleCreateFromCurrent = (customBlock) => { + const handleCreateCustomBlock = (customBlock) => { // Handle custom block creation console.log("Created custom block:", customBlock); }; - const handleCreateFromFile = () => { - // Handle file import logic - console.log("Create from file"); - }; - const editableNode = useMemo( () => getEditableNode(nodes, edges), [nodes, edges], @@ -616,7 +612,7 @@ export default function Main() {
<> - )} - + {editableNode && (
@@ -814,8 +810,7 @@ export default function Main() { onClose={() => setModalOpen(false)} nodes={nodesCustom} edges={edgesCustom} - onCreateFromFile={handleCreateFromFile} - onCreateFromCurrent={handleCreateFromCurrent} + onCreateCustomBlock={handleCreateCustomBlock} /> { + const { customBlocks } = useCustomBlocks(); + + const allNodeTypes = useMemo(() => { + const customNodeTypes = {}; + + customBlocks.forEach(block => { + customNodeTypes[`custom-${block.id}`] = CustomBlockNode; + }); + + return { + ...props.nodeTypes, + ...customNodeTypes + }; + }, [customBlocks, props.nodeTypes]); + + return ( + + ); +}; + +export default FlowWithCustomNodes; \ No newline at end of file diff --git a/UI/src/components/pages/mainPage/circuitsMenu.jsx b/UI/src/components/pages/mainPage/circuitsMenu.jsx index 2a76b0ed..a1bdcecb 100644 --- a/UI/src/components/pages/mainPage/circuitsMenu.jsx +++ b/UI/src/components/pages/mainPage/circuitsMenu.jsx @@ -36,7 +36,7 @@ export default function CircuitsMenu({ spawnCircuit, }) { const [openIndexes, setOpenIndexes] = useState([]); - const { customBlocks } = useCustomBlocks(); // Получаем блоки из контекста + const { customBlocks, isLoading } = useCustomBlocks(); const toggleItem = useCallback((index) => { setOpenIndexes((prevIndexes) => @@ -46,6 +46,20 @@ export default function CircuitsMenu({ ); }, []); + if (isLoading) { + return ( +
+
+
+

Menu

+
+
+

Loading custom blocks...

+
+
+ ); + } + const menuItems = [ { header: "Basic Logic Elements", @@ -59,8 +73,19 @@ export default function CircuitsMenu({ ], }, { - header: "Advanced Logic Elements", - gates: [], + header: "Custom Circuits", + gates: customBlocks.map((block) => ({ + id: `custom-${block.id}`, + label: block.name, + icon: (props) => ( + + ), + customData: block, + })), }, { header: "Pins", @@ -90,16 +115,14 @@ export default function CircuitsMenu({ // Обработчик для создания кастомного блока const handleSpawnCustomCircuit = useCallback( (nodeId) => { - // Ищем полные данные блока по ID const blockId = nodeId.replace("custom-", ""); const block = customBlocks.find((b) => b.id === blockId); if (block) { - // Вызываем функцию spawnCircuit с полными данными схемы - spawnCircuit(block.originalSchema); + spawnCircuit(`custom-${block.id}`); } }, - [customBlocks, spawnCircuit], + [customBlocks, spawnCircuit] ); return ( diff --git a/UI/src/components/pages/mainPage/createCustomBlockModal.jsx b/UI/src/components/pages/mainPage/createCustomBlockModal.jsx index 11926dfc..4cbeb82a 100644 --- a/UI/src/components/pages/mainPage/createCustomBlockModal.jsx +++ b/UI/src/components/pages/mainPage/createCustomBlockModal.jsx @@ -12,8 +12,7 @@ export default function CreateCustomBlockModal({ onClose, nodes, edges, - onCreateFromFile, - onCreateFromCurrent, + onCreateCustomBlock, }) { const [blockName, setBlockName] = useState(""); const [error, setError] = useState(""); @@ -34,7 +33,7 @@ export default function CreateCustomBlockModal({ setError(""); onClose(); - if (onCreateFromCurrent) onCreateFromCurrent(customBlock); + if (onCreateCustomBlock) onCreateCustomBlock(customBlock); alert(`Block "${blockName}" created successfully!`); } catch (err) { console.error("Error creating block:", err); diff --git a/UI/src/components/pages/mainPage/customCircuit.jsx b/UI/src/components/pages/mainPage/customCircuit.jsx index 442011e5..b393448e 100644 --- a/UI/src/components/pages/mainPage/customCircuit.jsx +++ b/UI/src/components/pages/mainPage/customCircuit.jsx @@ -1,10 +1,4 @@ import React, { createContext, useState, useContext, useEffect } from "react"; -import { IconCloseCross } from "../../../../assets/ui-icons.jsx"; -import { - LOG_LEVELS, - showToast, - showToastError, -} from "../../codeComponents/logger.jsx"; // Контекст для управления кастомными блоками const CustomBlocksContext = createContext(); @@ -33,12 +27,15 @@ export const CustomBlocksProvider = ({ children }) => { setCustomBlocks((prev) => prev.filter((block) => block.id !== blockId)); }; + const getBlockById = (id) => customBlocks.find(block => block.id === id); + return ( {children} @@ -48,104 +45,4 @@ export const CustomBlocksProvider = ({ children }) => { export const useCustomBlocks = () => useContext(CustomBlocksContext); -// Компонент для создания кастомных блоков -export default function CreateCustomBlock({ nodes, edges }) { - const [isModalOpen, setIsModalOpen] = useState(false); - const [blockName, setBlockName] = useState(""); - const [error, setError] = useState(""); - const { addBlock } = useCustomBlocks(); - - const handleCreateFromCurrent = () => { - if (!blockName.trim()) { - showToastError("Please enter a custom block name."); - return; - } - - try { - // Создаем кастомный блок - const customBlock = { - id: `custom_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 10)}`, - name: blockName.trim(), - inputs: nodes - .filter( - (node) => - node.type === "inputNodeSwitch" || - node.type === "inputNodeButton", - ) - .map((node) => ({ - id: node.id, - name: node.name || `input_${node.id.slice(0, 4)}`, - })), - outputs: nodes - .filter((node) => node.type === "outputNodeLed") - .map((node) => ({ - id: node.id, - name: node.name || `output_${node.id.slice(0, 4)}`, - })), - originalSchema: { nodes, edges }, - }; - - addBlock(customBlock); - - setBlockName(""); - setError(""); - setIsModalOpen(false); - - showToast( - `Block "${blockName}" created successfully!`, - "✅", - LOG_LEVELS.ERROR, - ); - } catch (err) { - console.error("Block creation error:", err); - setError(`Error: ${err.message}`); - } - }; - - return ( -
- - - {isModalOpen && ( -
-
-

Create custom block

- - - -
- - -
- - -
- setBlockName(e.target.value)} - placeholder="New custom block name" - required - /> - {error &&

{error}

} -
-
-
-
-
- )} -
- ); -} +export default CustomBlocksProvider; \ No newline at end of file diff --git a/UI/src/components/utils/customBlockUtils.js b/UI/src/components/utils/customBlockUtils.js index 964b1c64..a67acbb8 100644 --- a/UI/src/components/utils/customBlockUtils.js +++ b/UI/src/components/utils/customBlockUtils.js @@ -40,6 +40,7 @@ export const createCustomBlock = (nodes, edges, blockName) => { inputs, outputs, originalSchema: { nodes, edges }, + defaultPosition: { x: 0, y: 0 }, }; }; @@ -93,6 +94,11 @@ export const deleteCustomBlock = (blockId) => { * Находит кастомный блок по ID */ export const findCustomBlockById = (blockId) => { - const blocks = loadCustomBlocks(); - return blocks.find((block) => block.id === blockId); -}; + try { + const savedBlocks = JSON.parse(localStorage.getItem("customBlocks") || "[]"); + return savedBlocks.find(block => block.id === blockId); + } catch (error) { + console.error("Failed to find custom block:", error); + return null; + } +}; \ No newline at end of file diff --git a/UI/src/components/utils/onDrop.js b/UI/src/components/utils/onDrop.js index 40e8c366..6be6c3db 100644 --- a/UI/src/components/utils/onDrop.js +++ b/UI/src/components/utils/onDrop.js @@ -1,5 +1,6 @@ import { calculatePosition } from "./calculatePosition.js"; import { generateId } from "./generateId.js"; +import { findCustomBlockById } from "./customBlockUtils.js"; // Add import export function onDrop(event, reactFlowInstance, setNodes) { event.preventDefault(); @@ -13,14 +14,40 @@ export function onDrop(event, reactFlowInstance, setNodes) { const position = calculatePosition(rawPos, type); - const id = generateId(); - const newNode = { - id, - type, - position, - selected: true, - data: { customId: generateId() }, - }; + let newNode; + + // Handle custom blocks + if (type.startsWith('custom-')) { + const blockId = type.replace('custom-', ''); + const block = findCustomBlockById(blockId); + + if (block) { + newNode = { + id: generateId(), + type: 'customBlock', // Use this special type + position, + selected: true, + data: { + blockId: block.id, // Store block ID for lookup + label: block.name, // Display name + inputs: block.inputs, + outputs: block.outputs + }, + }; + } else { + console.error(`Custom block not found: ${blockId}`); + return; + } + } else { + // Standard node + newNode = { + id: generateId(), + type, + position, + selected: true, + data: { customId: generateId() }, + }; + } setNodes((nds) => nds.concat(newNode)); -} +} \ No newline at end of file diff --git a/UI/src/components/utils/spawnCircuit.js b/UI/src/components/utils/spawnCircuit.js index f9348c94..77a01b18 100644 --- a/UI/src/components/utils/spawnCircuit.js +++ b/UI/src/components/utils/spawnCircuit.js @@ -1,5 +1,6 @@ import { calculatePosition } from "./calculatePosition.js"; import { generateId } from "./generateId.js"; +import { findCustomBlockById } from "./customBlockUtils.js"; // Add import export function spawnCircuit(type, reactFlowInstance, setNodes) { if (!reactFlowInstance) return; @@ -11,16 +12,42 @@ export function spawnCircuit(type, reactFlowInstance, setNodes) { const position = calculatePosition(rawPos, type); - const id = generateId(); - const newNode = { - id: id, - type, - position, - selected: true, - data: { - customId: generateId(), - }, - }; + let newNode; + + // Handle custom blocks + if (type.startsWith('custom-')) { + const blockId = type.replace('custom-', ''); + const block = findCustomBlockById(blockId); + + if (block) { + newNode = { + id: generateId(), + type: 'customBlock', // Use this special type + position, + selected: true, + data: { + blockId: block.id, // Store block ID for lookup + label: block.name, // Display name + inputs: block.inputs, + outputs: block.outputs + }, + }; + } else { + console.error(`Custom block not found: ${blockId}`); + return; + } + } else { + // Standard node + newNode = { + id: generateId(), + type, + position, + selected: true, + data: { + customId: generateId(), + }, + }; + } setNodes((nds) => nds.concat(newNode)); -} +} \ No newline at end of file From 2d191e1532513d9286016ac710054b06879ebf05 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sun, 20 Jul 2025 22:37:32 +0300 Subject: [PATCH 141/152] appearance of custom nodes fix --- UI/src/CSS/circuits-menu.css | 4 ++-- UI/src/CSS/customBlock.css | 9 ++++++--- UI/src/components/circuits/customBlockNode.jsx | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/UI/src/CSS/circuits-menu.css b/UI/src/CSS/circuits-menu.css index 694dda35..6564d285 100644 --- a/UI/src/CSS/circuits-menu.css +++ b/UI/src/CSS/circuits-menu.css @@ -1,12 +1,12 @@ /*_______circuits menu_______*/ .circuits-menu { - /*width: 15.33rem;*/ + width: 15.33rem; overflow: auto; max-height: 70vh; z-index: 200; position: fixed; top: calc(2.5rem + 5vh); - left: -15.5rem; + left: -16rem; margin: 0.5rem 0 0 0.5rem; border-radius: 0.5rem; background-color: var(--menu-lighter); diff --git a/UI/src/CSS/customBlock.css b/UI/src/CSS/customBlock.css index 6338f1cb..a717665f 100644 --- a/UI/src/CSS/customBlock.css +++ b/UI/src/CSS/customBlock.css @@ -123,12 +123,15 @@ } .custom-block-node { - background-color: var(--main-2); - border: 1px solid var(--main-4); border-radius: 4px; + background: var(--menu-lighter); + color: var(--main-0); padding: 10px; min-width: 150px; - box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +.custom-block-node:hover { + background-color: var(--main-4); } .custom-block-header { diff --git a/UI/src/components/circuits/customBlockNode.jsx b/UI/src/components/circuits/customBlockNode.jsx index f222c0ef..d9cfc13c 100644 --- a/UI/src/components/circuits/customBlockNode.jsx +++ b/UI/src/components/circuits/customBlockNode.jsx @@ -15,7 +15,7 @@ const CustomBlockNode = ({ data }) => { type="target" position={Position.Left} id={`input-${idx}`} - style={{ top: `${(idx + 1) * 20}px`, background: '#555' }} + style={{ top: `${9}px`, left: `${-10}px` }} />
))} @@ -30,7 +30,7 @@ const CustomBlockNode = ({ data }) => { type="source" position={Position.Right} id={`output-${idx}`} - style={{ top: `${(idx + 1) * 20}px`, background: '#555' }} + style={{ top: `${9}px`, right: `${-10}px` }} />
))} From 55d606f32e4d7972b80dc850c02dbdca96a4fd95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 19:38:20 +0000 Subject: [PATCH 142/152] Automated formatting --- UI/src/CSS/customBlock.css | 8 +++--- .../components/circuits/customBlockNode.jsx | 6 ++--- UI/src/components/codeComponents/nodes.js | 2 +- UI/src/components/pages/mainPage.jsx | 2 +- .../pages/mainPage/FlowWithCustomNodes.jsx | 26 ++++++------------- .../pages/mainPage/circuitsMenu.jsx | 2 +- .../pages/mainPage/customCircuit.jsx | 4 +-- UI/src/components/utils/customBlockUtils.js | 8 +++--- UI/src/components/utils/onDrop.js | 10 +++---- UI/src/components/utils/spawnCircuit.js | 10 +++---- 10 files changed, 36 insertions(+), 42 deletions(-) diff --git a/UI/src/CSS/customBlock.css b/UI/src/CSS/customBlock.css index a717665f..13aa4bfa 100644 --- a/UI/src/CSS/customBlock.css +++ b/UI/src/CSS/customBlock.css @@ -147,13 +147,15 @@ justify-content: space-between; } -.inputs, .outputs { +.inputs, +.outputs { display: flex; flex-direction: column; gap: 8px; } -.input-group, .output-group { +.input-group, +.output-group { position: relative; display: flex; align-items: center; @@ -167,4 +169,4 @@ .output-label { margin-left: 5px; font-size: 0.8rem; -} \ No newline at end of file +} diff --git a/UI/src/components/circuits/customBlockNode.jsx b/UI/src/components/circuits/customBlockNode.jsx index d9cfc13c..b216aef6 100644 --- a/UI/src/components/circuits/customBlockNode.jsx +++ b/UI/src/components/circuits/customBlockNode.jsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { Handle, Position } from '@xyflow/react'; +import React from "react"; +import { Handle, Position } from "@xyflow/react"; const CustomBlockNode = ({ data }) => { return ( @@ -40,4 +40,4 @@ const CustomBlockNode = ({ data }) => { ); }; -export default CustomBlockNode; \ No newline at end of file +export default CustomBlockNode; diff --git a/UI/src/components/codeComponents/nodes.js b/UI/src/components/codeComponents/nodes.js index f4dac344..b833d3b4 100644 --- a/UI/src/components/codeComponents/nodes.js +++ b/UI/src/components/codeComponents/nodes.js @@ -8,7 +8,7 @@ import InputNodeSwitch from "../circuits/IOelemnts/switch.jsx"; import InputNodeButton from "../circuits/IOelemnts/button.jsx"; import OutputNodeLed from "../circuits/IOelemnts/led.jsx"; import SwitchNode from "../circuits/IOelemnts/switch.jsx"; -import CustomBlockNode from '../circuits/CustomBlockNode.jsx'; +import CustomBlockNode from "../circuits/CustomBlockNode.jsx"; export const nodeTypes = { andNode: AndNode, diff --git a/UI/src/components/pages/mainPage.jsx b/UI/src/components/pages/mainPage.jsx index f0463c03..f92a2b92 100644 --- a/UI/src/components/pages/mainPage.jsx +++ b/UI/src/components/pages/mainPage.jsx @@ -70,7 +70,7 @@ import { getEditableNode } from "../utils/getEditableNode.js"; import { handleNameChange } from "../utils/handleNameChange.js"; import CreateCustomBlockModal from "./mainPage/CreateCustomBlockModal.jsx"; import { CustomBlocksProvider } from "./mainPage/customCircuit.jsx"; -import FlowWithCustomNodes from './mainPage/FlowWithCustomNodes.jsx'; +import FlowWithCustomNodes from "./mainPage/FlowWithCustomNodes.jsx"; export const SimulateStateContext = createContext({ simulateState: "idle", diff --git a/UI/src/components/pages/mainPage/FlowWithCustomNodes.jsx b/UI/src/components/pages/mainPage/FlowWithCustomNodes.jsx index 6f6b0e21..3933bc60 100644 --- a/UI/src/components/pages/mainPage/FlowWithCustomNodes.jsx +++ b/UI/src/components/pages/mainPage/FlowWithCustomNodes.jsx @@ -1,12 +1,7 @@ -import React, { useMemo } from 'react'; -import { - Background, - Controls, - MiniMap, - ReactFlow, -} from '@xyflow/react'; -import CustomBlockNode from '../../circuits/customBlockNode.jsx'; -import { useCustomBlocks } from './customCircuit.jsx'; +import React, { useMemo } from "react"; +import { Background, Controls, MiniMap, ReactFlow } from "@xyflow/react"; +import CustomBlockNode from "../../circuits/customBlockNode.jsx"; +import { useCustomBlocks } from "./customCircuit.jsx"; const FlowWithCustomNodes = (props) => { const { customBlocks } = useCustomBlocks(); @@ -14,22 +9,17 @@ const FlowWithCustomNodes = (props) => { const allNodeTypes = useMemo(() => { const customNodeTypes = {}; - customBlocks.forEach(block => { + customBlocks.forEach((block) => { customNodeTypes[`custom-${block.id}`] = CustomBlockNode; }); return { ...props.nodeTypes, - ...customNodeTypes + ...customNodeTypes, }; }, [customBlocks, props.nodeTypes]); - return ( - - ); + return ; }; -export default FlowWithCustomNodes; \ No newline at end of file +export default FlowWithCustomNodes; diff --git a/UI/src/components/pages/mainPage/circuitsMenu.jsx b/UI/src/components/pages/mainPage/circuitsMenu.jsx index a1bdcecb..a6076309 100644 --- a/UI/src/components/pages/mainPage/circuitsMenu.jsx +++ b/UI/src/components/pages/mainPage/circuitsMenu.jsx @@ -122,7 +122,7 @@ export default function CircuitsMenu({ spawnCircuit(`custom-${block.id}`); } }, - [customBlocks, spawnCircuit] + [customBlocks, spawnCircuit], ); return ( diff --git a/UI/src/components/pages/mainPage/customCircuit.jsx b/UI/src/components/pages/mainPage/customCircuit.jsx index b393448e..9759c8f1 100644 --- a/UI/src/components/pages/mainPage/customCircuit.jsx +++ b/UI/src/components/pages/mainPage/customCircuit.jsx @@ -27,7 +27,7 @@ export const CustomBlocksProvider = ({ children }) => { setCustomBlocks((prev) => prev.filter((block) => block.id !== blockId)); }; - const getBlockById = (id) => customBlocks.find(block => block.id === id); + const getBlockById = (id) => customBlocks.find((block) => block.id === id); return ( { export const useCustomBlocks = () => useContext(CustomBlocksContext); -export default CustomBlocksProvider; \ No newline at end of file +export default CustomBlocksProvider; diff --git a/UI/src/components/utils/customBlockUtils.js b/UI/src/components/utils/customBlockUtils.js index a67acbb8..57db03c8 100644 --- a/UI/src/components/utils/customBlockUtils.js +++ b/UI/src/components/utils/customBlockUtils.js @@ -95,10 +95,12 @@ export const deleteCustomBlock = (blockId) => { */ export const findCustomBlockById = (blockId) => { try { - const savedBlocks = JSON.parse(localStorage.getItem("customBlocks") || "[]"); - return savedBlocks.find(block => block.id === blockId); + const savedBlocks = JSON.parse( + localStorage.getItem("customBlocks") || "[]", + ); + return savedBlocks.find((block) => block.id === blockId); } catch (error) { console.error("Failed to find custom block:", error); return null; } -}; \ No newline at end of file +}; diff --git a/UI/src/components/utils/onDrop.js b/UI/src/components/utils/onDrop.js index 6be6c3db..cad0065d 100644 --- a/UI/src/components/utils/onDrop.js +++ b/UI/src/components/utils/onDrop.js @@ -17,21 +17,21 @@ export function onDrop(event, reactFlowInstance, setNodes) { let newNode; // Handle custom blocks - if (type.startsWith('custom-')) { - const blockId = type.replace('custom-', ''); + if (type.startsWith("custom-")) { + const blockId = type.replace("custom-", ""); const block = findCustomBlockById(blockId); if (block) { newNode = { id: generateId(), - type: 'customBlock', // Use this special type + type: "customBlock", // Use this special type position, selected: true, data: { blockId: block.id, // Store block ID for lookup label: block.name, // Display name inputs: block.inputs, - outputs: block.outputs + outputs: block.outputs, }, }; } else { @@ -50,4 +50,4 @@ export function onDrop(event, reactFlowInstance, setNodes) { } setNodes((nds) => nds.concat(newNode)); -} \ No newline at end of file +} diff --git a/UI/src/components/utils/spawnCircuit.js b/UI/src/components/utils/spawnCircuit.js index 77a01b18..a2fca124 100644 --- a/UI/src/components/utils/spawnCircuit.js +++ b/UI/src/components/utils/spawnCircuit.js @@ -15,21 +15,21 @@ export function spawnCircuit(type, reactFlowInstance, setNodes) { let newNode; // Handle custom blocks - if (type.startsWith('custom-')) { - const blockId = type.replace('custom-', ''); + if (type.startsWith("custom-")) { + const blockId = type.replace("custom-", ""); const block = findCustomBlockById(blockId); if (block) { newNode = { id: generateId(), - type: 'customBlock', // Use this special type + type: "customBlock", // Use this special type position, selected: true, data: { blockId: block.id, // Store block ID for lookup label: block.name, // Display name inputs: block.inputs, - outputs: block.outputs + outputs: block.outputs, }, }; } else { @@ -50,4 +50,4 @@ export function spawnCircuit(type, reactFlowInstance, setNodes) { } setNodes((nds) => nds.concat(newNode)); -} \ No newline at end of file +} From 71437a0b45933c7b5ae949d6b09d912fbc318475 Mon Sep 17 00:00:00 2001 From: kostya2505 Date: Sat, 19 Jul 2025 22:37:16 +0300 Subject: [PATCH 143/152] text node introduced --- UI/assets/circuits-icons.jsx | 51 +++++++++++++++++++ .../circuits/advancedElements/TextNode.jsx | 35 +++++++++++++ UI/src/components/codeComponents/nodes.js | 4 +- .../pages/mainPage/circuitsMenu.jsx | 18 ++----- 4 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 UI/src/components/circuits/advancedElements/TextNode.jsx diff --git a/UI/assets/circuits-icons.jsx b/UI/assets/circuits-icons.jsx index 5350bbd7..b052c0d8 100644 --- a/UI/assets/circuits-icons.jsx +++ b/UI/assets/circuits-icons.jsx @@ -459,3 +459,54 @@ export const IconOutput = ({ SVGClassName, style }) => ( ); + +export const IconText = ({ SVGClassName, style }) => ( + + + TEXT + {/* Горизонтальная перекладина T */} + + {/* Вертикальная ножка T */} + + {/* Дополнительные горизонтальные строки текста */} + + + + + +); diff --git a/UI/src/components/circuits/advancedElements/TextNode.jsx b/UI/src/components/circuits/advancedElements/TextNode.jsx new file mode 100644 index 00000000..99de08e0 --- /dev/null +++ b/UI/src/components/circuits/advancedElements/TextNode.jsx @@ -0,0 +1,35 @@ +import { useState } from "react"; + +export default function TextNode({ data, selected }) { + const [text, setText] = useState(data.label || ""); + + return ( +
+