diff --git a/.gitignore b/.gitignore index 0ff8f19..ef7f65e 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,9 @@ doc/api/ *.out local.properties +**/main.cpp +!client/main.cpp +!client/client/test_server.cpp +!server/main.cpp -main.cpp - -*pb* \ No newline at end of file +*pb* diff --git a/CMakeLists.txt b/CMakeLists.txt index 821a0f3..af79c09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,34 +1,10 @@ cmake_minimum_required(VERSION 3.16) -project(EfficioTaskTracker LANGUAGES CXX) +project(test-server) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) -set(CMAKE_AUTOUIC ON) +#add_compile_options(-fsanitize=address,undefined,leak) +#add_link_options(-fsanitize=address,undefined,leak) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -find_package(Qt6 COMPONENTS Widgets Core Gui Sql REQUIRED) - -add_subdirectory(scripts) -add_subdirectory(database) -add_subdirectory(ui/main-window) -add_subdirectory(ui/authorization-windows) -add_subdirectory(ui/note-widget) +add_subdirectory(server) add_subdirectory(proto) -add_executable(EfficioTaskTracker main.cpp) - -target_link_libraries(EfficioTaskTracker PRIVATE - Qt6::Widgets - Qt6::Core - Qt6::Gui - Qt6::Sql - model-proto - efficio-rpc - Database - Scripts - AuthorizationWindows - MainWindow - NoteWidget -) \ No newline at end of file +add_subdirectory(client) diff --git a/README.md b/README.md index d2242e4..e1ebcb7 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,7 @@ git clone git@github.com:toximu/efficio-task-tracker.git ``` -2. Start **PostgreSQL** service - -```bash -sudo service postgresql start && sudo -u postgres psql -``` - -3. Create **efficio** user +2. Create **efficio** user ```SQL CREATE USER efficio WITH PASSWORD 'admin'; @@ -39,29 +33,50 @@ GRANT ALL PRIVILEGES ON DATABASE efficio TO efficio; \q ``` -4. Enter under **efficio** profile +3. Build ```bash -psql -U efficio -d efficio -h localhost +mkdir -p build && cd build +cmake .. +make ``` -> After that run the server on address **localhost** and **port** 5432 in your pgAdmin4 - -5. Build and start app +4. Run the server and client in different terminal windows ```bash -mkdir -p build && cd build -cmake .. -make -./EfficioTaskTracker -platform xcb +build/server/Server +build/client/EfficioTaskTracker +``` + +## Recent problems + +### 1) libpqxx not installed + +```shell +CMake Error at server/service/CMakelists.txt:8 (find_package): + By not providing "Findlibpqxx.cmake" in CMAKE_MODULE_PATH this project has + asked CMake to find a package configuration file provided by "libpqxx", + but CMake did not find one. ``` +1) Clone [libpqxx repository](https://github.com/jtv/libpqxx) into `server/` +2) `cd libpqxx/` then build and install it + +```shell +cmake . +cmake --build . +cmake --install . +``` + +Now try to build **EFFICIO** one more time + ## Technologies Used - Qt 6.8.2 - PostgreSQL 17.4 - CMake 3.28.3 -- VcXsrv server (XLaunch) +- gRPC 1.72.0 +- pqxx 7.9.2-1 ## License -This project is licensed under the **MIT License**. See the [LICENSE](https://github.com/toximu/efficio-task-tracker/blob/main/LICENSE) file for details. \ No newline at end of file +This project is licensed under the **MIT License**. See the [LICENSE](https://github.com/toximu/efficio-task-tracker/blob/main/LICENSE) file for details. diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 0000000..00dafc1 --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.16) + +project(EfficioTaskTracker LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 COMPONENTS Widgets Core Gui Sql REQUIRED) + +add_subdirectory(ui/main-window) +add_subdirectory(ui/note-widget) +add_subdirectory(ui/authorization-windows) +add_subdirectory(ui/theme-manager) +add_subdirectory(client) +add_executable(EfficioTaskTracker main.cpp) + +target_link_libraries(EfficioTaskTracker PRIVATE + model-proto + Qt6::Widgets + Qt6::Core + Qt6::Gui + Qt6::Sql + Database + Client + NoteWidget + MainWindow + AuthorizationWindows + ThemeManager + model-proto +) diff --git a/client/client/CMakeLists.txt b/client/client/CMakeLists.txt new file mode 100644 index 0000000..fbd0623 --- /dev/null +++ b/client/client/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.16) + +project(Service LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Protobuf CONFIG REQUIRED) +find_package(gRPC CONFIG REQUIRED) +find_package(Threads) + + +file(GLOB SOURCES "src/*.cpp") +file(GLOB HEADERS "include/*.h") + +add_library(Client STATIC ${SOURCES} ${HEADERS}) + +target_link_libraries(Client + model-proto + efficio-rpc + protobuf::libprotobuf + gRPC::grpc + gRPC::grpc++ + Service +) + +target_include_directories(Client + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_BINARY_DIR} +) + +add_executable(test-server test_server.cpp) +target_link_libraries(test-server Client) \ No newline at end of file diff --git a/client/client/include/auth_requests.h b/client/client/include/auth_requests.h new file mode 100644 index 0000000..599ecd0 --- /dev/null +++ b/client/client/include/auth_requests.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include "common_client_call.h" + +using Efficio_proto::Auth; +using Efficio_proto::AuthRequest; +using Efficio_proto::AuthResponse; +using Efficio_proto::User; +using grpc::Channel; +using grpc::ClientAsyncResponseReader; +using grpc::CompletionQueue; + +class AuthRequests { +public: + explicit AuthRequests( + const std::shared_ptr &channel, + CompletionQueue *cq + ); + + bool try_authenticate_user(User *user) const; + bool try_register_user(User *user) const; + +private: + class TryAuthenticateUserClientCall : public CommonClientCall { + public: + TryAuthenticateUserClientCall( + const AuthRequest &request, + CompletionQueue *cq, + Auth::Stub *stub + ); + void Proceed(bool ok) override; + AuthResponse get_reply(); + + private: + AuthResponse reply_; + ClientContext context; + Status status; + std::unique_ptr> responder_; + }; + + class TryRegisterUserClientCall : public CommonClientCall { + public: + TryRegisterUserClientCall( + const AuthRequest &request, + CompletionQueue *cq, + Auth::Stub *stub + ); + void Proceed(bool ok) override; + AuthResponse get_reply(); + + private: + AuthResponse reply_; + ClientContext context; + Status status; + std::unique_ptr> responder_; + }; + + std::unique_ptr stub_; + std::unique_ptr cq_; +}; \ No newline at end of file diff --git a/client/client/include/client_implementation.h b/client/client/include/client_implementation.h new file mode 100644 index 0000000..76c17a9 --- /dev/null +++ b/client/client/include/client_implementation.h @@ -0,0 +1,48 @@ +#ifndef CLIENTIMPLEMENTATION_H +#define CLIENTIMPLEMENTATION_H + +#include +#include +#include "auth_requests.h" +#include "update_requests.h" + +using Efficio_proto::User; +using grpc::Channel; +using grpc::CompletionQueue; + +class ClientImplementation { + CompletionQueue cq_; + std::shared_ptr channel_; + UpdateRequests update_requests_; + AuthRequests auth_requests_; + +public: + explicit ClientImplementation(const std::shared_ptr &channel); + std::thread complete_rpc_thread_; + + void CompleteRpc(); + std::shared_ptr get_channel(); + CompletionQueue *get_cq(); + + bool try_authenticate_user(User *user); + bool try_register_user(User *user); + + bool try_update_note(Note *note) const; + bool try_create_note(Note *note, const std::string& project_code) const; + bool try_fetch_note(Note *note) const; + + bool create_project( + Project *project, + const std::string &title, + const User &user + ); + bool get_project(Project *project, const std::string &code); + bool try_join_project( + Project *project, + const std::string &code, + const User &user + ); + bool try_leave_project(const std::string &code, const User &user); +}; + +#endif // CLIENTIMPLEMENTATION_H \ No newline at end of file diff --git a/client/client/include/common_client_call.h b/client/client/include/common_client_call.h new file mode 100644 index 0000000..0ba1d9b --- /dev/null +++ b/client/client/include/common_client_call.h @@ -0,0 +1,20 @@ +#ifndef COMMON_CLIENT_CALL_H +#define COMMON_CLIENT_CALL_H + +#include + +using grpc::ClientContext; +using grpc::Status; + +class CommonClientCall { +public: + explicit CommonClientCall() = default; + virtual ~CommonClientCall() = default; + + ClientContext context; + Status status; + + virtual void Proceed(bool = true) = 0; +}; + +#endif // COMMON_CLIENT_CALL_H diff --git a/client/client/include/update_requests.h b/client/client/include/update_requests.h new file mode 100644 index 0000000..d661b79 --- /dev/null +++ b/client/client/include/update_requests.h @@ -0,0 +1,100 @@ +#ifndef UPDATE_REQUESTS_H +#define UPDATE_REQUESTS_H + +#include +#include +#include +#include "common_client_call.h" + +using grpc::Channel; +using grpc::ClientAsyncResponseReader; +using grpc::CompletionQueue; + +using Efficio_proto::Note; +using Efficio_proto::Project; +using Efficio_proto::Storage; +using Efficio_proto::Update; +using Efficio_proto::User; + +using Efficio_proto::CreateNoteRequest; +using Efficio_proto::CreateNoteResponse; +using Efficio_proto::GetNoteRequest; +using Efficio_proto::GetNoteResponse; +using Efficio_proto::UpdateNoteRequest; +using Efficio_proto::UpdateNoteResponse; + +class UpdateRequests { +public: + class UpdateNoteClientCall final : public CommonClientCall { + UpdateNoteResponse reply_; + std::unique_ptr> + responder_; + + public: + UpdateNoteClientCall( + const UpdateNoteRequest &request, + grpc::CompletionQueue *cq, + const std::unique_ptr &stub + ); + void Proceed(bool ok) override; + UpdateNoteResponse get_reply(); + }; + + class GetNoteClientCall final : public CommonClientCall { + GetNoteResponse reply_; + std::unique_ptr> + responder_; + + public: + GetNoteClientCall( + const GetNoteRequest &request, + grpc::CompletionQueue *cq, + const std::unique_ptr &stub + ); + void Proceed(bool ok) override; + GetNoteResponse get_reply(); + }; + + class CreateNoteClientCall final : public CommonClientCall { + CreateNoteResponse reply_; + std::unique_ptr> + responder_; + + public: + CreateNoteClientCall( + const CreateNoteRequest &request, + grpc::CompletionQueue *cq, + const std::unique_ptr &stub + ); + void Proceed(bool ok) override; + CreateNoteResponse get_reply(); + }; + + bool try_update_note(Note *note) const; + bool try_fetch_note(Note *note) const; + bool try_create_note(Note *note, const std::string& project_code) const; + bool get_note(Note *note); + bool create_note(Note *note); + + bool get_project(Project &project, const std::string &code); + bool create_project( + Project &project, + const std::string &project_title, + const User &user + ); + bool try_leave_project(const std::string &code, const User &user); + bool try_join_project(Project &project, const std::string &code, const User &user); + + explicit UpdateRequests( + std::shared_ptr channel, + CompletionQueue *cq + ) + : stub_(Update::NewStub(channel)), cq_(cq) { + } + +private: + CompletionQueue *cq_; + std::unique_ptr stub_; +}; + +#endif // UPDATE_REQUESTS_H diff --git a/client/client/src/auth_requests.cpp b/client/client/src/auth_requests.cpp new file mode 100644 index 0000000..04021d1 --- /dev/null +++ b/client/client/src/auth_requests.cpp @@ -0,0 +1,119 @@ +#include +#include +#include + +using grpc::Channel; +using grpc::ClientAsyncResponseReader; +using grpc::ClientContext; +using grpc::CompletionQueue; +using grpc::Status; + +using Efficio_proto::Auth; +using Efficio_proto::AuthRequest; +using Efficio_proto::AuthResponse; +using Efficio_proto::User; + +AuthRequests::TryAuthenticateUserClientCall::TryAuthenticateUserClientCall( + const AuthRequest &request, + CompletionQueue *cq, + Auth::Stub *stub +) + : responder_(stub->AsyncTryAuthenticateUser(&context, request, cq)) { + context.set_deadline( + std::chrono::system_clock::now() + std::chrono::seconds(5) + ); + responder_->Finish(&reply_, &status, this); + std::cout << "[CLIENT]: AUTHENTICATE USER REQUEST SENT\n"; +} + +void AuthRequests::TryAuthenticateUserClientCall::Proceed(bool ok) { + if (!ok) { + std::cout << "[CLIENT WARNING]: Authentication RPC failed\n"; + } + delete this; +} + +AuthResponse AuthRequests::TryAuthenticateUserClientCall::get_reply() { + return reply_; +} + +AuthRequests::TryRegisterUserClientCall::TryRegisterUserClientCall( + const AuthRequest &request, + CompletionQueue *cq, + Auth::Stub *stub +) + : responder_(stub->AsyncTryRegisterUser(&context, request, cq)) { + context.set_deadline( + std::chrono::system_clock::now() + std::chrono::seconds(5) + ); + responder_->Finish(&reply_, &status, this); + std::cout << "[CLIENT]: REGISTER USER REQUEST SENT\n"; +} + +void AuthRequests::TryRegisterUserClientCall::Proceed(bool ok) { + if (!ok) { + std::cout << "[CLIENT WARNING]: Registration RPC failed\n"; + } + delete this; +} + +AuthResponse AuthRequests::TryRegisterUserClientCall::get_reply() { + return reply_; +} + +AuthRequests::AuthRequests( + const std::shared_ptr &channel, + CompletionQueue *cq +) + : stub_(Auth::NewStub(channel)), cq_(cq) { +} + +bool AuthRequests::try_authenticate_user(User *user) const { + AuthRequest request; + request.mutable_user()->set_login(user->login()); + request.mutable_user()->set_hashed_password(user->hashed_password()); + + const auto call = + new TryAuthenticateUserClientCall(request, cq_.get(), stub_.get()); + + void *tag; + bool ok = false; + if (!cq_->Next(&tag, &ok)) { + std::cout << "[CLIENT]: Completion queue failed\n"; + return false; + } + + if (!ok) { + std::cout << "[CLIENT WARNING]: Authentication failed\n"; + return false; + } + + user->CopyFrom(call->get_reply().user()); + std::cout << "[CLIENT]: AUTHENTICATED USER - " << user->login() << "\n"; + return true; +} + +bool AuthRequests::try_register_user(User *user) const { + AuthRequest request; + request.mutable_user()->set_login(user->login()); + request.mutable_user()->set_hashed_password(user->hashed_password()); + + const auto call = + new TryRegisterUserClientCall(request, cq_.get(), stub_.get()); + + void *tag; + bool ok = false; + if (!cq_->Next(&tag, &ok)) { + std::cout << "[CLIENT]: Completion queue failed\n"; + return false; + } + + if (!ok) { + std::cout << "[CLIENT WARNING]: Registration failed\n"; + return false; + } + + user->CopyFrom(call->get_reply().user()); + std::cout << "[CLIENT]: REGISTERED USER - " << user->login() << "\n"; + return true; +} \ No newline at end of file diff --git a/client/client/src/client_implementation.cpp b/client/client/src/client_implementation.cpp new file mode 100644 index 0000000..177d31f --- /dev/null +++ b/client/client/src/client_implementation.cpp @@ -0,0 +1,104 @@ +#include "client_implementation.h" +#include +#include +#include +#include +#include +#include "update_requests.h" +#include "update_service.h" + +using grpc::Channel; +using grpc::ClientAsyncResponseReader; +using grpc::ClientContext; +using grpc::CompletionQueue; +using grpc::Status; + +ClientImplementation::ClientImplementation( + const std::shared_ptr &channel +) + : channel_(channel), + update_requests_(channel, &cq_), + auth_requests_(channel, &cq_) { + complete_rpc_thread_ = + std::thread(&ClientImplementation::CompleteRpc, this); +} + +void ClientImplementation::CompleteRpc() { + void *got_tag; + bool ok = false; + + while (cq_.Next(&got_tag, &ok)) { + std::cout << "[CLIENT] : GET CALL FROM CQ" << std::endl; + CommonClientCall *call = static_cast(got_tag); + + assert(ok); + + if (call->status.ok()) { + std::cout << "[CLIENT] : START PROCEED" << std::endl; + call->Proceed(); + } else { + std::cout << "[CLIENT] : RPC FAILED" << std::endl; + } + + delete call; + } + std::cout << "complete rpc ended" << std::endl; +} + +std::shared_ptr ClientImplementation::get_channel() { + return channel_; +} + +CompletionQueue *ClientImplementation::get_cq() { + return &cq_; +} + +bool ClientImplementation::try_authenticate_user(User *user) { + return auth_requests_.try_authenticate_user(user); +} + +bool ClientImplementation::try_register_user(User *user) { + return auth_requests_.try_register_user(user); +} + +bool ClientImplementation::try_update_note(Note *note) const { + return update_requests_.try_update_note(note); +} + +bool ClientImplementation::try_create_note(Note *note, const std::string& project_code) const { + return update_requests_.try_create_note(note, project_code); +} + +bool ClientImplementation::try_fetch_note(Note *note) const { + return update_requests_.try_fetch_note(note); +} + +bool ClientImplementation::create_project( + Project *project, + const std::string &title, + const User &user +) { + return update_requests_.create_project(*project, title, user); +} + +bool ClientImplementation::get_project( + Project *project, + const std::string &code +) { + return update_requests_.get_project(*project, code); +} + +bool ClientImplementation::try_join_project( + Project *project, + const std::string &code, + const User &user +) { + return update_requests_.try_join_project(*project, code, user); +} + +bool ClientImplementation::try_leave_project( + const std::string &code, + const User &user +) { + return update_requests_.try_leave_project(code, user); +} diff --git a/client/client/src/update_requests.cpp b/client/client/src/update_requests.cpp new file mode 100644 index 0000000..f26e879 --- /dev/null +++ b/client/client/src/update_requests.cpp @@ -0,0 +1,277 @@ +#include "update_requests.h" +#include +#include +#include +#include +#include + +using grpc::Channel; +using grpc::ClientAsyncResponseReader; +using grpc::ClientContext; +using grpc::CompletionQueue; +using grpc::Status; + +using Efficio_proto::CreateProjectRequest; +using Efficio_proto::CreateProjectResponse; +using Efficio_proto::GetNoteRequest; +using Efficio_proto::GetNoteResponse; +using Efficio_proto::GetProjectRequest; +using Efficio_proto::GetProjectResponse; +using Efficio_proto::TryJoinProjectRequest; +using Efficio_proto::TryJoinProjectResponse; +using Efficio_proto::TryLeaveProjectRequest; +using Efficio_proto::TryLeaveProjectResponse; +using Efficio_proto::Update; +using Efficio_proto::User; + +bool UpdateRequests::get_project(Project &project, const std::string &code) { + GetProjectRequest request; + + request.set_code(code); + + GetProjectResponse response; + + ClientContext context; + + std::cout << "CLIENT : [get project] : sending request" << std::endl; + + Status status = stub_->GetProject(&context, request, &response); + + if (status.ok() && response.has_project()) { + std::cout << "CLIENT : [get project] : got project!" << std::endl; + project = std::move(*response.mutable_project()); + return true; + } + + if (response.has_error_text()) { + std::cout << "CLIENT : [create project] : error_text : " + << response.error_text() << std::endl; + } else { + std::cout << "CLIENT [create project] : status is not OK" << std::endl; + } + + return false; +} + +bool UpdateRequests::create_project( + Project &project, + const std::string &project_title, + const User &user +) { + CreateProjectRequest request; + request.set_project_title(project_title); + User *copy_user = new User(user); + request.set_allocated_user(copy_user); + + CreateProjectResponse response; + + ClientContext context; + + std::cout << "CLIENT : [create project] : sending request" << std::endl; + Status status = stub_->CreateProject(&context, request, &response); + + if (status.ok() && response.has_project()) { + std::cout << "CLIENT : [create project] : got project!" << std::endl; + project = std::move(*response.mutable_project()); + return true; + } + + if (response.has_error_text()) { + std::cout << "CLIENT [create project] {error text} : " + << response.error_text() << std::endl; + } else { + std::cout << "CLIENT [create project] status is not OK" << std::endl; + } + return false; +} + +bool UpdateRequests::try_leave_project( + const std::string &code, + const User &user +) { + TryLeaveProjectRequest request; + + request.set_code(code); + User *copy_user = new User(user); + request.set_allocated_user(copy_user); + + TryLeaveProjectResponse response; + ClientContext context; + std::cout << "CLIENT : [try leave project] : sending request" << std::endl; + Status status = stub_->TryLeaveProject(&context, request, &response); + + if (status.ok() && response.ok()) { + std::cout << "CLIENT : [try leave project] : left project" << std::endl; + return true; + } + + std::cout << "CLIENT : [try leave project] : can't leave project" + << std::endl; + return false; +} + +bool UpdateRequests::try_join_project( + Project &project, + const std::string &code, + const User &user +) { + TryJoinProjectRequest request; + request.set_code(code); + User *copy_user = new User(user); + request.set_allocated_user(copy_user); + TryJoinProjectResponse response; + ClientContext context; + std::cout << "CLIENT : [try join project] : sending request" << std::endl; + Status status = stub_->TryJoinProject(&context, request, &response); + + if (status.ok() && response.has_project()) { + std::cout << "CLIENT : [try join project] : got project!" << std::endl; + project = std::move(*response.mutable_project()); + return true; + } + if (response.has_error_text()) { + std::cout << "CLIENT : [try join project] : error_text : " + << response.error_text() << std::endl; + } else { + std::cout << "CLIENT : [try join project] : status is not OK" + << std::endl; + } + return false; +} + +UpdateRequests::UpdateNoteClientCall::UpdateNoteClientCall( + const UpdateNoteRequest &request, + CompletionQueue *cq, + const std::unique_ptr &stub +) + : responder_(stub->AsyncUpdateNote(&context, request, cq)) { + context.set_deadline( + std::chrono::system_clock::now() + std::chrono::seconds(5) + ); + responder_->Finish(&reply_, &status, this); + std::cout << "[CLIENT]: UPDATE NOTE REQUEST SENT\n"; +} + +void UpdateRequests::UpdateNoteClientCall::Proceed(bool ok) { + if (!ok) { + std::cout << "[CLIENT WARNING]: RPC failed\n"; + } + delete this; +} + +UpdateNoteResponse UpdateRequests::UpdateNoteClientCall::get_reply() { + return reply_; +} + +UpdateRequests::GetNoteClientCall::GetNoteClientCall( + const GetNoteRequest &request, + CompletionQueue *cq, + const std::unique_ptr &stub +) + : responder_(stub->AsyncGetNote(&context, request, cq)) { + context.set_deadline( + std::chrono::system_clock::now() + std::chrono::seconds(5) + ); + responder_->Finish(&reply_, &status, this); + std::cout << "[CLIENT]: FETCH NOTE REQUEST SENT\n"; +} + +void UpdateRequests::GetNoteClientCall::Proceed(const bool ok) { + if (!ok) { + std::cout << "[CLIENT WARNING]: RPC failed\n"; + } + delete this; +} + +GetNoteResponse UpdateRequests::GetNoteClientCall::get_reply() { + if (!status.ok()) { + throw std::runtime_error(status.error_message()); + } + return reply_; +} + +CreateNoteResponse UpdateRequests::CreateNoteClientCall::get_reply() { + return reply_; +} + +UpdateRequests::CreateNoteClientCall::CreateNoteClientCall( + const CreateNoteRequest &request, + CompletionQueue *cq, + const std::unique_ptr &stub + +) + : responder_(stub->AsyncCreateNote(&context, request, cq)) { + context.set_deadline( + std::chrono::system_clock::now() + std::chrono::seconds(5) + ); + responder_->Finish(&reply_, &status, this); + std::cout << "[CLIENT]: CREATE NOTE REQUEST SENT\n"; +} + +void UpdateRequests::CreateNoteClientCall::Proceed(const bool ok) { + if (!ok) { + std::cout << "[CLIENT WARNING]: RPC failed\n"; + } + delete this; +} + +bool UpdateRequests::try_update_note(Note *note) const { + UpdateNoteRequest request; + request.mutable_note()->CopyFrom(*note); + + const auto call = new UpdateNoteClientCall(request, cq_, stub_); + + void *tag; + bool ok = false; + if (!cq_->Next(&tag, &ok)) { + std::cout << "[CLIENT]: Completion queue failed\n"; + return false; + } + + if (!ok) { + std::cout << "[CLIENT WARNING]: no note in reply\n"; + return false; + } + + *note = call->get_reply().note(); + std::cout << "[CLIENT]: UPDATE NOTE - " << note->title() << "\n"; + return true; +} + +bool UpdateRequests::try_fetch_note(Note *note) const { + GetNoteRequest request; + request.set_id(note->id()); + + const auto call = new GetNoteClientCall(request, cq_, stub_); + + void *tag; + bool ok = false; + if (!cq_->Next(&tag, &ok)) { + std::cout << "[CLIENT]: Completion queue failed\n"; + return false; + } + + if (!ok || !call->get_reply().has_note()) { + std::cout << "[CLIENT WARNING]: no note in reply\n"; + return false; + } + + *note = call->get_reply().note(); + std::cout << "[CLIENT]: GOT NOTE - " << note->title() << "\n"; + return true; +} + +bool UpdateRequests::try_create_note(Note *note, const std::string& project_code) const { + CreateNoteRequest request; + request.set_project_code(project_code); + const auto call = new CreateNoteClientCall(request, cq_, stub_); + + void *tag; + bool ok = false; + if (!cq_->Next(&tag, &ok)) { + std::cout << "[CLIENT]: Completion queue failed\n"; + return false; + } + *note = call->get_reply().note(); + return true; +} diff --git a/client/client/test_server.cpp b/client/client/test_server.cpp new file mode 100644 index 0000000..b81c288 --- /dev/null +++ b/client/client/test_server.cpp @@ -0,0 +1,58 @@ +#include +#include "client_implementation.h" +#include "update_requests.h" + +using Efficio_proto::Project; +using grpc::Channel; + +int main() { + ClientImplementation client(grpc::CreateChannel( + "localhost:50051", grpc::InsecureChannelCredentials() + )); + + User user; + user.set_login("toximu"); + user.set_hashed_password("12345678"); + + // client.try_register_user(&user); ok + + Project *project = new Project; + + // client.create_project(project, "ttt", user); ok + + // std::cout << project->DebugString() << std::endl; + + // client.get_project(project, "JUSIBT"); + + // std::cout << project->DebugString() << std::endl; + + Note *note = new Note; + + // client.try_create_note(note); ok, but note should be added to project + // aswell + + note->set_id(1); + note->set_title("neww"); + note->set_text("some texttt"); + // client.try_fetch_note(note); // not ok, oshibka + // std::cout << "Note id : " << note->id() << ", title : " << note->title() + // << std::endl; + + // client.try_update_note(note); // ok + + std::cout << note->DebugString() << std::endl; + + User user2; + user2.set_login("usr"); + user2.set_hashed_password("12345678"); + + Project *project_to_join = new Project; + + // client.try_join_project(project_to_join, "ML05B3", user2); // ok + + // std::cout << project_to_join->DebugString() << std::endl; + + // client.try_leave_project("ML05B3", user2); // ok + + client.complete_rpc_thread_.join(); +} diff --git a/client/main.cpp b/client/main.cpp new file mode 100644 index 0000000..210fcc1 --- /dev/null +++ b/client/main.cpp @@ -0,0 +1,50 @@ +#include +#include +#include +#include +#include +#include +#include "applicationwindow.h" +#include "client_implementation.h" +// #include "login_window.h" +#include "mainwindow.h" + +int main(int argc, char *argv[]) { + QApplication application(argc, argv); + + QTranslator translator; + const QStringList ui_languages = QLocale::system().uiLanguages(); + for (const QString &locale : ui_languages) { + const QString base_name = "MainWindow_" + QLocale(locale).name(); + if (translator.load(":/i18n/" + base_name)) { + QApplication::installTranslator(&translator); + break; + } + } + + ClientImplementation client(grpc::CreateChannel( + "localhost:50051", grpc::InsecureChannelCredentials() + )); + + auto *app_window = new Ui::ApplicationWindow("EFFICIO"); + std::unique_ptr usr = std::make_unique(); + usr->set_login("toximu"); + usr->set_hashed_password("12345678"); + client.try_authenticate_user(usr.get()); + Ui::MainWindow window(app_window, std::move(usr), &client); + app_window->setCentralWidget(&window); + app_window->resize(800, 600); + app_window->show(); + + // auto *login_window = new LoginWindow(app_window); + // + // app_window->setCentralWidget(login_window); + // const QRect screen_geometry = + // QApplication::primaryScreen()->availableGeometry(); + // const int x = (screen_geometry.width() - login_window->width()) / 2; + // const int y = (screen_geometry.height() - login_window->height()) / 2; + // app_window->move(x, y); + // app_window->show(); + int exit_code = QApplication::exec(); + client.complete_rpc_thread_.join(); +} \ No newline at end of file diff --git a/ui/authorization-windows/CMakeLists.txt b/client/ui/authorization-windows/CMakeLists.txt similarity index 89% rename from ui/authorization-windows/CMakeLists.txt rename to client/ui/authorization-windows/CMakeLists.txt index e61f562..79bfc2f 100644 --- a/ui/authorization-windows/CMakeLists.txt +++ b/client/ui/authorization-windows/CMakeLists.txt @@ -40,4 +40,4 @@ target_include_directories(AuthorizationWindows $ ) -target_link_libraries(AuthorizationWindows PRIVATE Qt6::Widgets Qt6::Core Qt6::Gui Qt6::Sql Database Scripts MainWindow) \ No newline at end of file +target_link_libraries(AuthorizationWindows PRIVATE Qt6::Widgets Qt6::Core Qt6::Gui Qt6::Sql Database model-proto MainWindow ThemeManager Client) \ No newline at end of file diff --git a/client/ui/authorization-windows/include/login_window.h b/client/ui/authorization-windows/include/login_window.h new file mode 100644 index 0000000..1d72a04 --- /dev/null +++ b/client/ui/authorization-windows/include/login_window.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "client_implementation.h" +#include "database_manager.hpp" + +QT_BEGIN_NAMESPACE + +namespace Ui { +class LoginWindow; +} + +QT_END_NAMESPACE + +class LoginWindow final : public QWidget { + Q_OBJECT + +public: + explicit LoginWindow( + ClientImplementation *client, + QWidget *parent = nullptr + ); + ~LoginWindow() override; + + static const std::vector THEMES; + void handle_theme_changed(int theme); + +private slots: + void on_switch_mode_clicked(); + void on_push_enter_clicked(); + void on_switch_theme_clicked(); + +private: + Ui::LoginWindow *ui; + int counter_on_switch_theme_clicks = 0; + ClientImplementation *client_; +}; \ No newline at end of file diff --git a/client/ui/authorization-windows/include/login_window_style_sheet.h b/client/ui/authorization-windows/include/login_window_style_sheet.h new file mode 100644 index 0000000..f8af17e --- /dev/null +++ b/client/ui/authorization-windows/include/login_window_style_sheet.h @@ -0,0 +1,377 @@ +#pragma once + +#include "ui_login_window.h" + +namespace Ui { +QString login_window_light_autumn_theme = R"( + QWidget { + background-color: #f5f5f5; + } + + QLabel { + background-color: transparent; + font-family: 'Arial'; + font-size: 13px; + color: #089083; + padding: 1px; + } + + QPushButton#push_enter { + font-family: 'Arial'; + border-radius: 10px; + background-color: #fea36b; + color: white; + padding: 5px 10px; + } + + QPushButton#switch_mode { + font-family: 'Arial'; + border-radius: 10px; + background-color: white; + color: #fea36b; + padding: 5px 10px; + } + + QPushButton#push_enter:hover { + background-color: #d58745; + } + + QPushButton#switch_mode:hover { + background-color: #dadada; + } + + QLineEdit { + border-radius: 10px; + border: 1px solid white; + background: white; + color: black; + padding: 5px; + } + + QLineEdit::placeholder { + color: #727272; + } + + QPushButton#switch_theme { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + border-radius: 7px; + padding: 0; + margin-bottom: 2px; + background-color: transparent; + border: 2px solid #089083; + } + QPushButton::hover#switch_theme { + background-color: #089083; + } + QPushButton::pressed#switch_theme { + background-color:rgb(7, 110, 100); + } + + )"; + +QString login_window_dark_autumn_theme = R"( + QWidget { + background-color: #202020; + } + + QLabel { + background-color: transparent; + font-family: 'Arial'; + font-size: 13px; + color: #089083; + padding: 1px; + } + + QPushButton#push_enter { + font-family: 'Arial'; + border-radius: 10px; + background-color: #fea36b; + color: #263238; + padding: 5px 10px; + } + + QPushButton#push_enter:hover { + background-color:rgb(225, 133, 76); + } + + QPushButton#switch_mode { + font-family: 'Arial'; + border-radius: 10px; + background-color: #089083; + color: white; + padding: 5px 10px; + } + + QPushButton#switch_mode:hover { + background-color: #01635d; + } + + QLineEdit { + border-radius: 10px; + border: 1px solid rgb(0, 0, 0); + background:rgb(0, 0, 0); + color: #727272; + padding: 5px; + } + + QLineEdit::placeholder { + color: #727272; + } + + QPushButton#switch_theme { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + border-radius: 7px; + padding: 0; + margin-bottom: 2px; + background-color: transparent; + border: 2px solid #089083; + } + QPushButton::hover#switch_theme { + background-color: #089083; + } + QPushButton::pressed#switch_theme { + background-color:rgb(13, 93, 85); + } + )"; + +QString login_window_light_purple_theme = R"( + QWidget { + background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, + stop:0 #9882B9, stop:0.5 rgb(176, 157, 205), stop:1 rgb(103, 88, 126)); + margin: 0; + padding: 0; + border: none; + } + + QLabel { + background-color: transparent; + font-family: 'Arial'; + font-size: 13px; + color: rgb(42, 10, 25); + padding: 1px; + } + + QPushButton#push_enter { + font-family: 'Arial'; + border-radius: 10px; + background-color: #722548; + color:rgb(206, 193, 224); + padding: 5px 10px; + } + + QPushButton#push_enter:hover { + background-color:rgb(98, 27, 59) + } + + QPushButton#switch_mode { + font-family: 'Arial'; + border-radius: 10px; + background-color: rgb(42, 10, 25); + color: rgb(218, 207, 235); + padding: 5px 10px; + } + + QPushButton#switch_mode:hover { + background-color:rgb(27, 6, 16); + } + + QLineEdit { + border-radius: 10px; + border: 1px solid rgb(221, 210, 238); + background: rgb(221, 210, 238); + color: #221932; + padding: 5px; + } + + QLineEdit::placeholder { + color: white; + } + + QPushButton#switch_theme { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + border-radius: 7px; + padding: 0; + margin-bottom: 2px; + background-color: transparent; + border: 2px solid #060407; + } + QPushButton::hover#switch_theme { + background-color: #060407; + } + QPushButton::pressed#switch_theme { + background-color:rgb(2, 0, 2); + } + )"; + +QString login_window_dark_purple_theme = R"( + QWidget { + background-color:rgb(9, 6, 10); + } + + QLabel { + background-color: transparent; + font-family: 'Arial'; + font-size: 13px; + color: #9882B9; + padding: 1px; + } + + QPushButton#push_enter { + font-family: 'Arial'; + border-radius: 10px; + background-color: #722548; + color: #060407; + padding: 5px 10px; + } + + QPushButton#push_enter:hover { + background-color:rgb(98, 27, 59) + } + + QPushButton#switch_mode { + font-family: 'Arial'; + border-radius: 10px; + background-color: rgb(42, 10, 25); + color: #9882B9; + padding: 5px 10px; + } + + QPushButton#switch_mode:hover { + background-color:rgb(27, 6, 16); + } + + QLineEdit { + border-radius: 10px; + border: 1px solid #221932; + background: #221932; + color: #9882B9; + padding: 5px; + } + + QLineEdit::placeholder { + color: white; + } + + QPushButton#switch_theme { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + border-radius: 7px; + padding: 0; + margin-bottom: 2px; + background-color: transparent; + border: 2px solid #9882B9; + } + QPushButton::hover#switch_theme { + background-color: #9882B9; + } + QPushButton::pressed#switch_theme { + background-color:rgb(113, 93, 143); + } + + )"; + +QString login_window_blue_theme = R"( + QWidget { + background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, + stop:0 #173C4C, stop:0.5 #326D6C, stop:1 #07142B); + margin: 0; + padding: 0; + border: none; + } + + QLabel { + background-color: transparent; + font-family: 'Arial'; + font-size: 13px; + color: #BDD1BD; + padding: 1px; + } + + QPushButton#push_enter { + font-family: 'Arial'; + border-radius: 10px; + background-color: #568F7C; + color: #BDD1BD; + padding: 5px 10px; + border: none; + font-weight: bold; + } + + QPushButton#push_enter:hover { + background-color: #326D6C; + } + + QPushButton#push_enter:pressed { + background-color: #07142B; + } + + QPushButton#switch_mode { + font-family: 'Arial'; + border-radius: 10px; + background-color: #326D6C; + color: #BDD1BD; + padding: 5px 10px; + border: 1px solid #568F7C; + } + + QPushButton#switch_mode:hover { + background-color: #568F7C; + color: #07142B; + } + + QLineEdit { + border-radius: 10px; + border: 1px solid #568F7C; + background: #07142B; + color: #BDD1BD; + padding: 5px; + selection-background-color: #326D6C; + } + + QLineEdit::placeholder { + color: #85B093; + opacity: 0.7; + } + + QPushButton#switch_theme { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + border-radius: 7px; + padding: 0; + margin-bottom: 2px; + background-color: transparent; + border: 2px solid #85B093; + } + + QPushButton#switch_theme:hover { + background-color: #85B093; + } + + QPushButton#switch_theme:pressed { + background-color:rgb(107, 141, 118); + } + )"; +} // namespace Ui \ No newline at end of file diff --git a/client/ui/authorization-windows/include/registration_window.h b/client/ui/authorization-windows/include/registration_window.h new file mode 100644 index 0000000..1929bbe --- /dev/null +++ b/client/ui/authorization-windows/include/registration_window.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include "client_implementation.h" +#include "database_manager.hpp" + +QT_BEGIN_NAMESPACE + +namespace Ui { +class RegistrationWindow; +} + +QT_END_NAMESPACE + +class RegistrationWindow final : public QWidget { + Q_OBJECT + +public: + static const std::vector THEMES; + + explicit RegistrationWindow( + ClientImplementation *client, + QWidget *parent = nullptr + ); + ~RegistrationWindow() override; + bool is_strong_and_valid_password(const QString &password); + void handle_theme_changed(int theme); + +private slots: + void on_switch_mode_clicked(); + void on_push_registration_clicked(); + void on_switch_theme_clicked(); + +private: + Ui::RegistrationWindow *ui; + int counter_on_switch_theme_clicks = 0; + ClientImplementation *client_; +}; \ No newline at end of file diff --git a/client/ui/authorization-windows/include/registration_window_style_sheet.h b/client/ui/authorization-windows/include/registration_window_style_sheet.h new file mode 100644 index 0000000..c907b27 --- /dev/null +++ b/client/ui/authorization-windows/include/registration_window_style_sheet.h @@ -0,0 +1,373 @@ +#pragma once + +#include "ui_registration_window.h" + +namespace Ui { +QString registration_window_light_autumn_theme = R"( + QWidget { + background-color: #f5f5f5; + } + + QLabel { + font-family: 'Arial'; + background-color: transparent; + font-size: 13px; + color: #089083; + padding: 1px; + } + + QPushButton#push_registration { + font-family: 'Arial'; + border-radius: 10px; + background-color: #fea36b; + color: white; + padding: 5px 10px; + } + + QPushButton#switch_mode { + font-family: 'Arial'; + border-radius: 10px; + background-color: white; + color: #fea36b; + padding: 5px 10px; + } + + QPushButton#push_registration:hover { + background-color: #d58745; + } + + QPushButton#switch_mode:hover { + background-color: #dadada; + } + + QLineEdit { + border-radius: 10px; + border: 1px solid white; + background: white; + color: black; + padding: 5px; + } + + QLineEdit::placeholder { + color: #727272; + } + + QPushButton#switch_theme { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + border-radius: 7px; + padding: 0; + margin-bottom: 2px; + background-color: transparent; + border: 2px solid #089083; + } + QPushButton::hover#switch_theme { + background-color: #089083; + } + QPushButton::pressed#switch_theme { + background-color:rgb(7, 110, 100); + } + )"; + +QString registration_window_dark_autumn_theme = R"( + QWidget { + background-color: #202020; + } + + QLabel { + font-family: 'Arial'; + background-color: transparent; + font-size: 13px; + color: #089083; + padding: 1px; + } + + QPushButton#push_registration { + font-family: 'Arial'; + border-radius: 10px; + background-color: #fea36b; + color: #263238; + padding: 5px 10px; + } + + QPushButton#push_registration:hover { + background-color: rgb(225, 133, 76); + } + + QPushButton#switch_mode { + font-family: 'Arial'; + border-radius: 10px; + background-color: #089083; + color: white; + padding: 5px 10px; + } + + QPushButton#switch_mode:hover { + background-color: #01635d; + } + + QLineEdit { + border-radius: 10px; + border: 1px solid rgb(0, 0, 0); + background: rgb(0, 0, 0); + color: #727272; + padding: 5px; + } + + QLineEdit::placeholder { + color: #727272; + } + + QPushButton#switch_theme { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + border-radius: 7px; + padding: 0; + margin-bottom: 2px; + background-color: transparent; + border: 2px solid #089083; + } + QPushButton::hover#switch_theme { + background-color: #089083; + } + QPushButton::pressed#switch_theme { + background-color:rgb(13, 93, 85); + } + )"; + +QString registration_window_light_purple_theme = R"( + QWidget { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #9882B9, stop:0.5 rgb(176, 157, 205), stop:1 rgb(103, 88, 126)); + margin: 0; + padding: 0; + border: none; + } + + QLabel { + font-family: 'Arial'; + background-color: transparent; + font-size: 13px; + color: rgb(42, 10, 25); + padding: 1px; + } + + QPushButton#push_registration { + font-family: 'Arial'; + border-radius: 10px; + background-color: #722548; + color: rgb(206, 193, 224); + padding: 5px 10px; + } + + QPushButton#push_registration:hover { + background-color: rgb(98, 27, 59); + } + + QPushButton#switch_mode { + font-family: 'Arial'; + border-radius: 10px; + background-color: rgb(42, 10, 25); + color: rgb(218, 207, 235); + padding: 5px 10px; + } + + QPushButton#switch_mode:hover { + background-color: rgb(27, 6, 16); + } + + QLineEdit { + border-radius: 10px; + border: 1px solid rgb(221, 210, 238); + background: rgb(221, 210, 238); + color: #221932; + padding: 5px; + } + + QLineEdit::placeholder { + color: white; + } + + QPushButton#switch_theme { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + border-radius: 7px; + padding: 0; + margin-bottom: 2px; + background-color: transparent; + border: 2px solid #060407; + } + QPushButton::hover#switch_theme { + background-color: #060407; + } + QPushButton::pressed#switch_theme { + background-color:rgb(2, 0, 2); + } + )"; + +QString registration_window_dark_purple_theme = R"( + QWidget { + background-color: rgb(9, 6, 10); + } + + QLabel { + font-family: 'Arial'; + background-color: transparent; + font-size: 13px; + color: #9882B9; + padding: 1px; + } + + QPushButton#push_registration { + font-family: 'Arial'; + border-radius: 10px; + background-color: #722548; + color: #060407; + padding: 5px 10px; + } + + QPushButton#push_registration:hover { + background-color: rgb(98, 27, 59); + } + + QPushButton#switch_mode { + font-family: 'Arial'; + border-radius: 10px; + background-color: rgb(42, 10, 25); + color: #9882B9; + padding: 5px 10px; + } + + QPushButton#switch_mode:hover { + background-color: rgb(27, 6, 16); + } + + QLineEdit { + border-radius: 10px; + border: 1px solid #221932; + background: #221932; + color: #9882B9; + padding: 5px; + } + + QLineEdit::placeholder { + color: white; + } + + QPushButton#switch_theme { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + border-radius: 7px; + padding: 0; + margin-bottom: 2px; + background-color: transparent; + border: 2px solid #9882B9; + } + QPushButton::hover#switch_theme { + background-color: #9882B9; + } + QPushButton::pressed#switch_theme { + background-color:rgb(113, 93, 143); + } + )"; + +QString registration_window_blue_theme = R"( + QWidget { + background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, + stop:0 #173C4C, stop:0.5 #326D6C, stop:1 #07142B) !important; + margin: 0; + padding: 0; + border: none; + } + + QLabel { + font-family: 'Arial'; + background-color: transparent; + font-size: 13px; + color: #BDD1BD; + padding: 1px; + } + + QPushButton#push_registration { + font-family: 'Arial'; + border-radius: 10px; + background-color: #568F7C; + color: #BDD1BD; + padding: 5px 10px; + border: none; + font-weight: bold; + } + + QPushButton#push_registration:hover { + background-color: #326D6C; + } + + QPushButton#push_registration:pressed { + background-color: #07142B; + } + + QPushButton#switch_mode { + font-family: 'Arial'; + border-radius: 10px; + background-color: #326D6C; + color: #BDD1BD; + padding: 5px 10px; + border: 1px solid #568F7C; + } + + QPushButton#switch_mode:hover { + background-color: #568F7C; + color: #07142B; + } + + QLineEdit { + border-radius: 10px; + border: 1px solid #568F7C; + background: #07142B; + color: #BDD1BD; + padding: 5px; + selection-background-color: #326D6C; + } + + QLineEdit::placeholder { + color: #85B093; + opacity: 0.7; + } + + QPushButton#switch_theme { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + border-radius: 7px; + padding: 0; + margin-bottom: 2px; + background-color: transparent; + border: 2px solid #85B093; + } + QPushButton#switch_theme:hover { + background-color: #85B093; + } + QPushButton#switch_theme:pressed { + background-color:rgb(107, 141, 118); + } + )"; +} // namespace Ui \ No newline at end of file diff --git a/ui/authorization-windows/login_window_ru_RU.ts b/client/ui/authorization-windows/login_window_ru_RU.ts similarity index 100% rename from ui/authorization-windows/login_window_ru_RU.ts rename to client/ui/authorization-windows/login_window_ru_RU.ts diff --git a/client/ui/authorization-windows/src/login_window.cpp b/client/ui/authorization-windows/src/login_window.cpp new file mode 100644 index 0000000..59b8c61 --- /dev/null +++ b/client/ui/authorization-windows/src/login_window.cpp @@ -0,0 +1,132 @@ +#include "login_window.h" +#include +#include +#include +#include +#include +#include "client_implementation.h" +#include "login_window_style_sheet.h" +#include "model-proto/model.pb.h" +#include "registration_window.h" +#include "theme_manager.h" + +using Efficio_proto::Storage; + +const std::vector LoginWindow::THEMES = { + Ui::login_window_light_autumn_theme, Ui::login_window_dark_autumn_theme, + Ui::login_window_dark_purple_theme, Ui::login_window_light_purple_theme, + Ui::login_window_blue_theme +}; + +LoginWindow::LoginWindow(ClientImplementation *client, QWidget *parent) + : QWidget(parent), ui(new Ui::LoginWindow), client_(client) { + ui->setupUi(this); + + setFixedSize(380, 480); + ui->input_login->setPlaceholderText("Введите логин:"); + ui->input_password->setPlaceholderText("Введите пароль:"); + + ui->input_password->setEchoMode(QLineEdit::Password); + handle_theme_changed(ThemeManager::get_instance()->get_current_theme()); + + connect( + ui->switch_theme, &QPushButton::clicked, this, + &LoginWindow::on_switch_theme_clicked, Qt::UniqueConnection + ); + + connect( + ui->switch_mode, &QPushButton::clicked, this, + &LoginWindow::on_switch_mode_clicked + ); + connect( + ui->push_enter, &QPushButton::clicked, this, + &LoginWindow::on_push_enter_clicked + ); + connect( + ThemeManager::get_instance(), &ThemeManager::on_theme_changed, this, + &LoginWindow::handle_theme_changed + ); +} + +void LoginWindow::handle_theme_changed(int theme) { + this->setStyleSheet(THEMES[theme]); +} + +void LoginWindow::on_switch_theme_clicked() { + if (this->counter_on_switch_theme_clicks++ % 2) { + const int next_theme = + (ThemeManager::get_instance()->get_current_theme() + 1) % 5; + ThemeManager::get_instance()->apply_theme(next_theme); + } +} + +LoginWindow::~LoginWindow() { + delete ui; +} + +void LoginWindow::on_switch_mode_clicked() { + QWidget *parent = this->parentWidget(); + + auto *app_window = qobject_cast(parent); + + if (QWidget *old = app_window->centralWidget()) { + old->deleteLater(); + } + Storage storage; + auto *registration_window = new RegistrationWindow(client_, app_window); + + app_window->setCentralWidget(registration_window); + QRect screenGeometry = QApplication::primaryScreen()->availableGeometry(); + int x = (screenGeometry.width() - registration_window->width()) / 2; + int y = (screenGeometry.height() - registration_window->height()) / 2; + app_window->move(x, y); + + this->close(); +} + +void LoginWindow::on_push_enter_clicked() { + if (this->counter_on_switch_theme_clicks++ % 2) { + const QString login = ui->input_login->text(); + const QString password = ui->input_password->text(); + + auto user = new User(); + user->set_login(login.toStdString()); + user->set_hashed_password(password.toStdString()); + + if (!login.isEmpty() && !password.isEmpty()) { + if (login.size() > 50) { + QMessageBox::warning( + this, "Ошибка", + "Длина логина не должна превышать пятидесяти символов" + ); + } else if (password.size() > 50) { + QMessageBox::warning( + this, "Ошибка", + "Длина пароля не должна превышать пятидесяти символов" + ); + } else if (client_->try_authenticate_user(user)) { + QMessageBox::information( + this, "Вход", "Вы успешно вошли! Добро пожаловать :)" + ); + QWidget *parent = this->parentWidget(); + + const QMainWindow *app_window = + qobject_cast(parent); + + if (QWidget *old = app_window->centralWidget()) { + old->deleteLater(); + } + + this->close(); + } else { + QMessageBox::warning( + this, "Ошибка ввода данных", "Неверный логин или пароль!" + ); + } + } else { + QMessageBox::warning( + this, "Ошибка ввода данных", "Пожалуйста, заполните все поля!" + ); + } + } +} \ No newline at end of file diff --git a/client/ui/authorization-windows/src/registration_window.cpp b/client/ui/authorization-windows/src/registration_window.cpp new file mode 100644 index 0000000..3aaabdd --- /dev/null +++ b/client/ui/authorization-windows/src/registration_window.cpp @@ -0,0 +1,195 @@ +#include "registration_window.h" +#include +#include +#include +#include +#include +#include +#include "login_window.h" +#include "registration_window_style_sheet.h" +#include "theme_manager.h" + +using Efficio_proto::Storage; + +const std::vector RegistrationWindow::THEMES = { + Ui::registration_window_light_autumn_theme, + Ui::registration_window_dark_autumn_theme, + Ui::registration_window_dark_purple_theme, + Ui::registration_window_light_purple_theme, + Ui::registration_window_blue_theme +}; + +RegistrationWindow::RegistrationWindow( + ClientImplementation *client, + QWidget *parent +) + : QWidget(parent), ui(new Ui::RegistrationWindow), client_(client) { + ui->setupUi(this); + + setFixedSize(380, 480); + + ui->create_login->setPlaceholderText("Введите логин:"); + ui->create_password->setPlaceholderText("Введите пароль:"); + ui->repeat_password->setPlaceholderText("Повторите пароль:"); + ui->create_password->setEchoMode(QLineEdit::Password); + ui->repeat_password->setEchoMode(QLineEdit::Password); + + setAttribute(Qt::WA_StyledBackground, true); + + connect( + ui->switch_theme, &QPushButton::clicked, this, + &RegistrationWindow::on_switch_theme_clicked, Qt::UniqueConnection + ); + + connect( + ui->push_registration, &QPushButton::clicked, this, + &RegistrationWindow::on_push_registration_clicked, Qt::UniqueConnection + ); + connect( + ui->switch_mode, &QPushButton::clicked, this, + &RegistrationWindow::on_switch_mode_clicked, Qt::UniqueConnection + ); + connect( + ThemeManager::get_instance(), &ThemeManager::on_theme_changed, this, + &RegistrationWindow::handle_theme_changed + ); + handle_theme_changed(ThemeManager::get_instance()->get_current_theme()); +} + +void RegistrationWindow::handle_theme_changed(const int theme) { + this->setStyleSheet(THEMES[theme]); +} + +void RegistrationWindow::on_switch_theme_clicked() { + if ((this->counter_on_switch_theme_clicks++) % 2) { + int next_theme = + (ThemeManager::get_instance()->get_current_theme() + 1) % 5; + ThemeManager::get_instance()->apply_theme(next_theme); + } +} + +RegistrationWindow::~RegistrationWindow() { + delete ui; +} + +void RegistrationWindow::on_switch_mode_clicked() { + QWidget *parent = this->parentWidget(); + + auto *app_window = qobject_cast(parent); + + if (QWidget *old = app_window->centralWidget()) { + old->deleteLater(); + } + Storage storage; + auto *login_window = new LoginWindow(client_, app_window); + + app_window->setCentralWidget(login_window); + const QRect screenGeometry = + QApplication::primaryScreen()->availableGeometry(); + const int x = (screenGeometry.width() - login_window->width()) / 2; + const int y = (screenGeometry.height() - login_window->height()) / 2; + app_window->move(x, y); + + this->close(); +} + +bool RegistrationWindow::is_strong_and_valid_password(const QString &password) { + if (password.length() < 8) { + QMessageBox::warning( + nullptr, "Ошибка: недостаточно надежный пароль", + "Пароль должен содержать не менее восьми символов" + ); + return false; + } + + bool has_digit = false; + bool has_latin_letter = false; + + for (const QChar symbol : password) { + if (!symbol.isLetter() && !symbol.isDigit()) { + QMessageBox::warning( + nullptr, "Ошибка", + "Пароль должен содержать только символы латиницы и цифры" + ); + return false; + } + + if (symbol.isLetter()) { + has_latin_letter = true; + } else if (symbol.isDigit()) { + has_digit = true; + } + } + + if (!has_latin_letter) { + QMessageBox::warning( + nullptr, "Ошибка: недостаточно надежный пароль", + "Пароль должен содержать хотя бы одну букву" + ); + return false; + } + if (!has_digit) { + QMessageBox::warning( + nullptr, "Ошибка: недостаточно надежный пароль", + "Пароль должен содержать хотя бы одну цифру" + ); + return false; + } + + return true; +} + +void RegistrationWindow::on_push_registration_clicked() { + if (this->counter_on_switch_theme_clicks++ % 2) { + const QString created_login = ui->create_login->text(); + const QString created_password = ui->create_password->text(); + const QString repeated_password = ui->repeat_password->text(); + + if (!created_login.isEmpty() && !created_password.isEmpty() && + !repeated_password.isEmpty()) { + if (created_password != repeated_password) { + QMessageBox::warning(this, "Ошибка", "Пароли не совпадают!"); + } else if (created_login.size() > 50) { + QMessageBox::warning( + this, "Ошибка", + "Длина логина не должна превышать пятидесяти символов" + ); + } else if (created_password.size() > 50) { + QMessageBox::warning( + this, "Ошибка", + "Длина пароля не должна превышать пятидесяти символов" + ); + } else if (is_strong_and_valid_password(created_password)) { + auto user = new User(); + user->set_login(created_login.toStdString()); + user->set_hashed_password(created_password.toStdString()); + + const int try_register_user = client_->try_register_user(user); + if (try_register_user == 0) { + QMessageBox::warning( + this, "Ошибка", + "Извините, внутренняя ошибка с базами данных." + ); + } else if (try_register_user == -1) { + QMessageBox::warning( + this, "Ошибка", + "Пользователь с таким именем уже существует. " + "Пожалуйста, " + "придумайте другое!" + ); + } else { + QMessageBox::information( + this, "Регистрация", + "Вы успешно зарегистрировались! Пожалуйста, выполните " + "вход." + ); + on_switch_mode_clicked(); + } + } + } else { + QMessageBox::warning( + this, "Ошибка", "Пожалуйста, заполните все поля." + ); + } + } +} \ No newline at end of file diff --git a/ui/authorization-windows/ui/login_window.ui b/client/ui/authorization-windows/ui/login_window.ui similarity index 76% rename from ui/authorization-windows/ui/login_window.ui rename to client/ui/authorization-windows/ui/login_window.ui index 02ffce3..a9ea57e 100644 --- a/ui/authorization-windows/ui/login_window.ui +++ b/client/ui/authorization-windows/ui/login_window.ui @@ -22,7 +22,7 @@ 501 - + 120 @@ -39,10 +39,10 @@ border-radius: 10px; Войти - + - 140 + 133 130 91 41 @@ -55,7 +55,7 @@ border-radius: 10px; Вход - + 62 @@ -71,7 +71,7 @@ border-radius: 10px; Логин - + 62 @@ -87,7 +87,7 @@ border-radius: 10px; Пароль - + 80 @@ -104,6 +104,28 @@ border-radius: 10px; Еще нет аккаунта? Зарегистрируйтесь! + + + + 350 + 455 + 20 + 20 + + + + + 20 + 20 + + + + + 20 + 20 + + + diff --git a/ui/authorization-windows/ui/registration_window.ui b/client/ui/authorization-windows/ui/registration_window.ui similarity index 67% rename from ui/authorization-windows/ui/registration_window.ui rename to client/ui/authorization-windows/ui/registration_window.ui index d0b20d4..0ba3e34 100644 --- a/ui/authorization-windows/ui/registration_window.ui +++ b/client/ui/authorization-windows/ui/registration_window.ui @@ -13,12 +13,34 @@ Dialog - + - 90 + 350 + 455 + 20 + 20 + + + + + 20 + 20 + + + + + 20 + 20 + + + + + + + 85 290 - 201 + 210 51 @@ -30,7 +52,7 @@ border-radius: 10px; Зарегистрироваться - + 110 @@ -47,7 +69,7 @@ border-radius: 10px; Уже есть аккаунт? Войдите! - + 60 @@ -57,7 +79,7 @@ border-radius: 10px; - + 60 @@ -67,7 +89,7 @@ border-radius: 10px; - + 60 @@ -77,10 +99,10 @@ border-radius: 10px; - + - 100 + 94 90 201 51 diff --git a/ui/main-window/CMakeLists.txt b/client/ui/main-window/CMakeLists.txt similarity index 91% rename from ui/main-window/CMakeLists.txt rename to client/ui/main-window/CMakeLists.txt index c8e1f85..eb96399 100644 --- a/ui/main-window/CMakeLists.txt +++ b/client/ui/main-window/CMakeLists.txt @@ -29,11 +29,12 @@ target_include_directories(MainWindow PUBLIC ) target_link_libraries(MainWindow PRIVATE + model-proto Qt6::Widgets Qt6::Core Qt6::Gui Qt6::Sql - Scripts Database NoteWidget + Client ) \ No newline at end of file diff --git a/ui/main-window/MainWindow_en_US.ts b/client/ui/main-window/MainWindow_en_US.ts similarity index 100% rename from ui/main-window/MainWindow_en_US.ts rename to client/ui/main-window/MainWindow_en_US.ts diff --git a/ui/main-window/include/applicationwindow.h b/client/ui/main-window/include/applicationwindow.h similarity index 74% rename from ui/main-window/include/applicationwindow.h rename to client/ui/main-window/include/applicationwindow.h index ccb14f8..dbbe82e 100644 --- a/ui/main-window/include/applicationwindow.h +++ b/client/ui/main-window/include/applicationwindow.h @@ -8,7 +8,7 @@ class ApplicationWindow : public QMainWindow { Q_OBJECT public: - explicit ApplicationWindow(std::string window_name); + explicit ApplicationWindow(const std::string &window_name); signals: }; diff --git a/ui/main-window/include/bottombar.h b/client/ui/main-window/include/bottombar.h similarity index 94% rename from ui/main-window/include/bottombar.h rename to client/ui/main-window/include/bottombar.h index cc7945c..97f49a1 100644 --- a/ui/main-window/include/bottombar.h +++ b/client/ui/main-window/include/bottombar.h @@ -1,24 +1,24 @@ -#ifndef BOTTOMBAR_H -#define BOTTOMBAR_H - -#include -#include -#include -#include - -namespace Ui { -class BottomBar : public QWidget { - QHBoxLayout *main_layout_; - QLabel *project_name_; - QLabel *username_; - -public: - BottomBar( - QWidget *parent_, - std::string username_, - std::string project_name_ - ); -}; -} // namespace Ui - +#ifndef BOTTOMBAR_H +#define BOTTOMBAR_H + +#include +#include +#include +#include + +namespace Ui { +class BottomBar : public QWidget { + QHBoxLayout *main_layout_; + QLabel *project_name_; + QLabel *username_; + +public: + BottomBar( + QWidget *parent_, + std::string username_, + std::string project_name_ + ); +}; +} // namespace Ui + #endif // BOTTOMBAR_H \ No newline at end of file diff --git a/ui/main-window/include/main_window_style.hpp b/client/ui/main-window/include/main_window_style.hpp similarity index 99% rename from ui/main-window/include/main_window_style.hpp rename to client/ui/main-window/include/main_window_style.hpp index 3e594c1..c6afa54 100644 --- a/ui/main-window/include/main_window_style.hpp +++ b/client/ui/main-window/include/main_window_style.hpp @@ -133,7 +133,6 @@ QPushButton { )"; - } // namespace Ui #endif // MAIN_WINDOW_STYLE_HPP \ No newline at end of file diff --git a/ui/main-window/include/mainwindow.h b/client/ui/main-window/include/mainwindow.h similarity index 63% rename from ui/main-window/include/mainwindow.h rename to client/ui/main-window/include/mainwindow.h index e5242f7..6e920d9 100644 --- a/ui/main-window/include/mainwindow.h +++ b/client/ui/main-window/include/mainwindow.h @@ -1,6 +1,7 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H +#include #include #include #include @@ -9,14 +10,17 @@ #include #include #include "bottombar.h" +#include "client_implementation.h" #include "notelist.h" #include "projectlist.h" -#include "storage.hpp" + +using namespace Efficio_proto; namespace Ui { class MainWindow : public QWidget { Q_OBJECT - std::string username; + ClientImplementation *client_; + std::unique_ptr user_; QVBoxLayout *main_layout_; BottomBar *top_bar_; QHBoxLayout *content_layout_; @@ -24,19 +28,22 @@ class MainWindow : public QWidget { NoteList *note_list_; QWidget *content_widget_; QPushButton *new_project_button_; + QPushButton *join_project_button_; QPushButton *new_note_button_; - project_storage_model::Storage *storage_; + + friend ProjectList; private slots: - void add_project(); + void create_project(); + void add_project_by_code(); void add_note(); public: explicit MainWindow( - QWidget *parent = nullptr, - std::string username = "none", - project_storage_model::Storage *storage = nullptr + QWidget *parent, + std::unique_ptr user, + ClientImplementation *client ); }; } // namespace Ui diff --git a/ui/main-window/include/notelist.h b/client/ui/main-window/include/notelist.h similarity index 60% rename from ui/main-window/include/notelist.h rename to client/ui/main-window/include/notelist.h index f406536..1cfbc7b 100644 --- a/ui/main-window/include/notelist.h +++ b/client/ui/main-window/include/notelist.h @@ -1,10 +1,13 @@ #ifndef NOTELIST_H #define NOTELIST_H +#include #include #include #include #include -#include "note.hpp" +#include "client_implementation.h" + +using namespace Efficio_proto; namespace Ui { class NoteList : public QWidget { @@ -15,12 +18,16 @@ class NoteList : public QWidget { std::vector vertical_layouts_; + ClientImplementation *client_; + int note_counter_ = 0; + const std::size_t notes_per_row = 3; + public: - void add_note_widget(const project_storage_model::Note *note); + void add_note_widget(Note *note); void clear_note_list(); - NoteList(QWidget *parent); + NoteList(QWidget *parent, ClientImplementation *client); public slots: void load_project_notes(QListWidgetItem *project); diff --git a/client/ui/main-window/include/notewidget.h b/client/ui/main-window/include/notewidget.h new file mode 100644 index 0000000..ad6dad5 --- /dev/null +++ b/client/ui/main-window/include/notewidget.h @@ -0,0 +1,38 @@ +#ifndef NOTEWIDGET_H +#define NOTEWIDGET_H + +#include +#include +#include +#include +#include +#include "client_implementation.h" + +using namespace Efficio_proto; + +namespace Ui { +class NoteWidget : public QWidget { + Q_OBJECT + Note * const model_note_; + QVBoxLayout *main_layout_; + QPushButton *open_button_; + QLabel *title_label_; + QHBoxLayout *tags_layout_; + std::vector tag_labels_; + ClientImplementation *client_; + +public: + explicit NoteWidget( + QWidget *parent, + Note *model_note, + ClientImplementation *client + ); +private: + void update_tags(); + +private slots: + void good_resize(); + void open_note_window() ; +}; +} // namespace Ui +#endif // NOTEWIDGET_H \ No newline at end of file diff --git a/ui/main-window/include/projectitem.h b/client/ui/main-window/include/projectitem.h similarity index 62% rename from ui/main-window/include/projectitem.h rename to client/ui/main-window/include/projectitem.h index 8b4a4a2..3979bd6 100644 --- a/ui/main-window/include/projectitem.h +++ b/client/ui/main-window/include/projectitem.h @@ -1,19 +1,21 @@ #ifndef PROJECTITEM_H #define PROJECTITEM_H +#include #include #include #include "notelist.h" -#include "project.hpp" + +using namespace Efficio_proto; namespace Ui { class ProjectItem : public QListWidgetItem { - project_storage_model::Project *project_; + Project *project_; friend NoteList; friend class MainWindow; public: - ProjectItem(QListWidget *listview, project_storage_model::Project *project); + ProjectItem(QListWidget *listview, Project *project); }; } // namespace Ui #endif // PROJECTITEM_H \ No newline at end of file diff --git a/ui/main-window/include/projectlist.h b/client/ui/main-window/include/projectlist.h similarity index 61% rename from ui/main-window/include/projectlist.h rename to client/ui/main-window/include/projectlist.h index d221695..f2a470c 100644 --- a/ui/main-window/include/projectlist.h +++ b/client/ui/main-window/include/projectlist.h @@ -1,17 +1,18 @@ #ifndef PROJECTLIST_H #define PROJECTLIST_H +#include #include #include -#include "project.hpp" -#include "storage.hpp" + +using namespace Efficio_proto; namespace Ui { class ProjectList : public QListWidget { Q_OBJECT friend class MainWindow; - void add_project(project_storage_model::Project *project); - void load_projects(project_storage_model::Storage *storage); + void add_project(Project *project); + void load_projects(Storage *storage); public: explicit ProjectList(QWidget *parent = nullptr); diff --git a/ui/main-window/src/applicationwindow.cpp b/client/ui/main-window/src/applicationwindow.cpp similarity index 70% rename from ui/main-window/src/applicationwindow.cpp rename to client/ui/main-window/src/applicationwindow.cpp index 62c7bfe..9f7857d 100644 --- a/ui/main-window/src/applicationwindow.cpp +++ b/client/ui/main-window/src/applicationwindow.cpp @@ -1,14 +1,16 @@ #include "applicationwindow.h" +#include "notelist.h" #include -#include "mainwindow.h" namespace Ui { -ApplicationWindow::ApplicationWindow(std::string window_name_) +ApplicationWindow::ApplicationWindow(const std::string &window_name_) : QMainWindow{nullptr} { this->setObjectName("ApplicationWindow"); this->setAttribute(Qt::WA_StyledBackground); this->setWindowTitle(window_name_.c_str()); + + } } // namespace Ui \ No newline at end of file diff --git a/ui/main-window/src/bottombar.cpp b/client/ui/main-window/src/bottombar.cpp similarity index 96% rename from ui/main-window/src/bottombar.cpp rename to client/ui/main-window/src/bottombar.cpp index ee63c71..6bc3f51 100644 --- a/ui/main-window/src/bottombar.cpp +++ b/client/ui/main-window/src/bottombar.cpp @@ -1,31 +1,31 @@ -#include "bottombar.h" -#include -#include -#include -#include - -namespace Ui { -BottomBar::BottomBar( - QWidget *parent, - std::string username, - std::string project_name -) - : QWidget(parent), - main_layout_(new QHBoxLayout()), - username_(new QLabel(username.c_str())), - project_name_(new QLabel(project_name.c_str())) { - this->setObjectName("BottomBar"); - this->setLayout(main_layout_); - this->setFixedHeight(40); - - project_name_->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); - username_->setAlignment(Qt::AlignVCenter | Qt::AlignRight); - - this->setSizePolicy( - QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum) - ); - - main_layout_->addWidget(project_name_); - main_layout_->addWidget(username_); -} +#include "bottombar.h" +#include +#include +#include +#include + +namespace Ui { +BottomBar::BottomBar( + QWidget *parent, + std::string username, + std::string project_name +) + : QWidget(parent), + main_layout_(new QHBoxLayout()), + username_(new QLabel(username.c_str())), + project_name_(new QLabel(project_name.c_str())) { + this->setObjectName("BottomBar"); + this->setLayout(main_layout_); + this->setFixedHeight(40); + + project_name_->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + username_->setAlignment(Qt::AlignVCenter | Qt::AlignRight); + + this->setSizePolicy( + QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum) + ); + + main_layout_->addWidget(project_name_); + main_layout_->addWidget(username_); +} } // namespace Ui \ No newline at end of file diff --git a/ui/main-window/src/mainwindow.cpp b/client/ui/main-window/src/mainwindow.cpp similarity index 54% rename from ui/main-window/src/mainwindow.cpp rename to client/ui/main-window/src/mainwindow.cpp index 907f902..4eb708d 100644 --- a/ui/main-window/src/mainwindow.cpp +++ b/client/ui/main-window/src/mainwindow.cpp @@ -1,41 +1,41 @@ #include "mainwindow.h" +#include #include #include #include #include #include #include +#include #include #include -#include #include #include "bottombar.h" -#include "lr_dao.hpp" #include "main_window_style.hpp" -#include "note_dao.hpp" #include "notelist.h" -#include "project.hpp" -#include "project_dao.hpp" #include "projectitem.h" #include "projectlist.h" +using namespace Efficio_proto; + namespace Ui { MainWindow::MainWindow( QWidget *parent, - std::string username, - project_storage_model::Storage *storage + std::unique_ptr user, + ClientImplementation *client ) : QWidget(parent), - username(username), + client_(client), + user_(std::move(user)), main_layout_(new QVBoxLayout(this)), - top_bar_(new BottomBar(this, username, "EFFICIO :: Таск-Трекер")), + top_bar_(new BottomBar(this, user_->login(), "EFFICIO :: Таск-Трекер")), content_layout_(new QHBoxLayout(this)), project_list_(new ProjectList(this)), - note_list_(new NoteList(this)), + note_list_(new NoteList(this, client)), content_widget_(new QWidget(this)), - new_project_button_(new QPushButton("Новый проект", this)), - new_note_button_(new QPushButton("Новая заметка", this)), - storage_(storage) { + new_project_button_(new QPushButton("Создать", this)), + join_project_button_(new QPushButton("Добавить", this)), + new_note_button_(new QPushButton("Новая заметка", this)) { this->setObjectName("main-window"); this->setAttribute(Qt::WA_StyledBackground); this->setMinimumSize(QSize(800, 600)); @@ -46,10 +46,15 @@ MainWindow::MainWindow( main_layout_->addWidget(content_widget_); content_widget_->setLayout(content_layout_); auto right_layout = new QVBoxLayout(content_widget_); + right_layout->addWidget(project_list_); - right_layout->addWidget(new_project_button_); + + auto project_button_layout = new QHBoxLayout(content_widget_); + project_button_layout->addWidget(new_project_button_); + project_button_layout->addWidget(join_project_button_); + right_layout->addLayout(project_button_layout); right_layout->addWidget(new_note_button_); - QScrollArea* scrollArea = new QScrollArea(content_widget_); + QScrollArea *scrollArea = new QScrollArea(content_widget_); scrollArea->setWidgetResizable(true); scrollArea->setWidget(note_list_); content_layout_->addWidget(scrollArea, Qt::AlignRight); @@ -57,7 +62,7 @@ MainWindow::MainWindow( main_layout_->addWidget(content_widget_); this->setLayout(main_layout_); - this->project_list_->load_projects(storage); + this->project_list_->load_projects(user_->mutable_storage()); connect( project_list_, &QListWidget::itemClicked, note_list_, @@ -68,26 +73,38 @@ MainWindow::MainWindow( ); connect( new_project_button_, &QPushButton::clicked, this, - &Ui::MainWindow::add_project + &Ui::MainWindow::create_project + ); + connect( + join_project_button_, &QPushButton::clicked, this, + &MainWindow::add_project_by_code ); } -void MainWindow::add_project() { +void MainWindow::create_project() { bool ok; QString name_of_project = QInputDialog::getText( nullptr, "Название проекта:", "Введите название", QLineEdit::Normal, "", &ok ); + if (ok) { - int id = 0; + Project *project = user_->mutable_storage()->add_projects(); + client_->create_project(project, name_of_project.toStdString(), *user_); + project_list_->add_project(project); + } +} - if (DB::ProjectDAO::create_project(name_of_project.toStdString(), id)) { - LRDao::add_project_to_user(username, id); - auto &project = storage_->add_project( - Project(id, name_of_project.toStdString(), "") - ); - project_list_->add_project(&project); - } +void MainWindow::add_project_by_code() { + bool ok; + QString code = QInputDialog::getText( + nullptr, "Код", "Введите код:", QLineEdit::Normal, "", &ok + ); + + if (ok) { + Project *project = user_->mutable_storage()->add_projects(); + client_->try_join_project(project,code.toStdString() , *user_); + project_list_->add_project(project); } } @@ -95,14 +112,9 @@ void MainWindow::add_note() { auto project_item = dynamic_cast(project_list_->currentItem()); if (project_item) { - if (int id = 0; NoteDao::initialize_note(id)) { - DB::ProjectDAO::add_note_to_project( - project_item->project_->get_id(), id - ); - auto ¬e = - project_item->project_->add_note({id, "Пустая заметка", ""}); - note_list_->add_note_widget(¬e); - } + Note *note = project_item->project_->add_notes(); + client_->try_create_note(note, project_item->project_->code()); + note_list_->add_note_widget(note); } else { QMessageBox msg; msg.setText("Проект не выбран!"); diff --git a/ui/main-window/src/notelist.cpp b/client/ui/main-window/src/notelist.cpp similarity index 66% rename from ui/main-window/src/notelist.cpp rename to client/ui/main-window/src/notelist.cpp index 4af7558..d542848 100644 --- a/ui/main-window/src/notelist.cpp +++ b/client/ui/main-window/src/notelist.cpp @@ -4,20 +4,20 @@ #include #include #include -#include "note.hpp" #include "notewidget.h" #include "projectitem.h" namespace Ui { -NoteList::NoteList(QWidget *parent) +NoteList::NoteList(QWidget *parent, ClientImplementation *client) : QWidget(parent), + client_(client), main_layout_(new QHBoxLayout(this)), - vertical_layouts_(std::vector()){ - + vertical_layouts_(std::vector()) { this->setAttribute(Qt::WA_StyledBackground); this->setObjectName("NoteList"); this->setLayout(main_layout_); - vertical_layouts_.resize(4, nullptr); + + vertical_layouts_.resize(notes_per_row, nullptr); for (auto &layout : vertical_layouts_) { layout = new QVBoxLayout(this); @@ -25,15 +25,15 @@ NoteList::NoteList(QWidget *parent) } } -void NoteList::add_note_widget(const project_storage_model::Note *note) { - auto current_layout = vertical_layouts_[note_counter_ % 4]; +void NoteList::add_note_widget(Note *note) { + auto current_layout = vertical_layouts_[note_counter_ % notes_per_row]; if (current_layout->count() > 1) { current_layout->removeItem( current_layout->itemAt(current_layout->count() - 1) ); } - vertical_layouts_[note_counter_ % 4]->addWidget( - new NoteWidget(this, note), 0, Qt::AlignTop + vertical_layouts_[note_counter_ % notes_per_row]->addWidget( + new NoteWidget(this, note, client_), 0, Qt::AlignTop ); current_layout->addStretch(); note_counter_++; @@ -42,12 +42,10 @@ void NoteList::add_note_widget(const project_storage_model::Note *note) { void NoteList::load_project_notes(QListWidgetItem *project) { ProjectItem *p = dynamic_cast(project); assert(p != nullptr); - qDebug() << "Адрес проекта" - << QString::fromStdString(p->project_->get_name()) << ":" - << p->project_; this->clear_note_list(); + client_->get_project(p->project_, p->project_->code()); note_counter_ = 0; - for (const auto ¬e : p->project_->get_notes()) { + for (Note ¬e : *p->project_->mutable_notes()) { this->add_note_widget(¬e); } } diff --git a/client/ui/main-window/src/notewidget.cpp b/client/ui/main-window/src/notewidget.cpp new file mode 100644 index 0000000..40d155a --- /dev/null +++ b/client/ui/main-window/src/notewidget.cpp @@ -0,0 +1,85 @@ +#include "notewidget.h" +#include +#include +#include +// #include +// "../../../../cmake-build-just-run/client/ui/note-widget/NoteWidget_autogen/include/ui_note_edit_dialog.h" +#include +#include "note_edit_dialog.h" + +namespace Ui { +NoteWidget::NoteWidget( + QWidget *parent, + Note *model_note, + ClientImplementation *client +) + : QWidget(parent), + model_note_(model_note), + main_layout_(new QVBoxLayout(this)), + open_button_(new QPushButton("Открыть")), + tags_layout_(new QHBoxLayout(this)), + tag_labels_(), + client_(client) { + this->setObjectName("NoteWidget"); + this->setMinimumWidth(100); + + this->setFixedHeight(110); + title_label_ = new QLabel(model_note_->title().c_str(), this); + + title_label_->setStyleSheet( + "color: rgb(33, 44, 50);" + ); + + main_layout_->addWidget(title_label_); + main_layout_->addLayout(tags_layout_); + update_tags(); + // title_label_->setWordWrap(true); + + main_layout_->addWidget(open_button_); + + connect( + open_button_, &QPushButton::clicked, this, &NoteWidget::open_note_window + ); + this->setLayout(main_layout_); + this->setAttribute(Qt::WA_StyledBackground); +} + +void NoteWidget::open_note_window() { + client_->try_fetch_note(model_note_); + auto dialog = new ::NoteEditDialog( + client_, const_cast(qobject_cast(this)), + const_cast(model_note_) + ); + dialog->setAttribute(Qt::WA_DeleteOnClose); + update_tags(); + dialog->exec(); + + title_label_->setText(model_note_->title().c_str()); + update_tags(); + main_layout_->update(); +} + +void NoteWidget::update_tags() { + for (auto tag : tag_labels_) { + tags_layout_->removeWidget(tag); + delete tag; + } + tag_labels_.clear(); + + for (auto tag : model_note_->tags()) { + QLabel *tag_label = new QLabel(this); + tag_label->setText(tag.text().c_str()); + tag_label->setStyleSheet( + ::NoteEditDialog::create_tag_style_sheet(tag.color()) + ); + tag_label->resize(tag_label->sizeHint()); + tag_labels_.push_back(tag_label); + tags_layout_->addWidget(tag_label); + } +} + +void NoteWidget::good_resize() { + setMaximumWidth(reinterpret_cast(parent())->width() / 3); +} + +} // namespace Ui diff --git a/client/ui/main-window/src/projectitem.cpp b/client/ui/main-window/src/projectitem.cpp new file mode 100644 index 0000000..cc5d2cb --- /dev/null +++ b/client/ui/main-window/src/projectitem.cpp @@ -0,0 +1,8 @@ +#include "projectitem.h" +#include + +namespace Ui { +ProjectItem::ProjectItem(QListWidget *list_view, Project *project) + : project_(project), QListWidgetItem(project->title().c_str(), list_view) { +} +} // namespace Ui \ No newline at end of file diff --git a/ui/main-window/src/projectlist.cpp b/client/ui/main-window/src/projectlist.cpp similarity index 50% rename from ui/main-window/src/projectlist.cpp rename to client/ui/main-window/src/projectlist.cpp index 3bd7828..f5591fa 100644 --- a/ui/main-window/src/projectlist.cpp +++ b/client/ui/main-window/src/projectlist.cpp @@ -1,23 +1,22 @@ #include "projectlist.h" #include #include "mainwindow.h" -#include "project.hpp" #include "projectitem.h" namespace Ui { ProjectList::ProjectList(QWidget *parent) : QListWidget{parent} { this->setObjectName("ProjectList"); - this->setFixedWidth(200); + this->setFixedWidth(300); } -void ProjectList::add_project(project_storage_model::Project *project) { +void ProjectList::add_project(Project *project) { this->addItem(new ProjectItem(static_cast(this), project)); } -void ProjectList::load_projects(project_storage_model::Storage *storage) { - for (project_storage_model::Project &pr : storage->get_projects()) { - add_project(&pr); +void ProjectList::load_projects(Storage *storage) { + for (Project &project : *storage->mutable_projects()) { + add_project(&project); } } diff --git a/ui/note-widget/CMakeLists.txt b/client/ui/note-widget/CMakeLists.txt similarity index 91% rename from ui/note-widget/CMakeLists.txt rename to client/ui/note-widget/CMakeLists.txt index d062724..cefe109 100644 --- a/ui/note-widget/CMakeLists.txt +++ b/client/ui/note-widget/CMakeLists.txt @@ -37,4 +37,4 @@ target_include_directories(NoteWidget $ ) -target_link_libraries(NoteWidget PRIVATE Qt6::Widgets Qt6::Core Qt6::Gui Qt6::Sql Database Scripts) \ No newline at end of file +target_link_libraries(NoteWidget PRIVATE Qt6::Widgets Qt6::Core Qt6::Gui Qt6::Sql Client Database model-proto) \ No newline at end of file diff --git a/ui/note-widget/NoteWidgetEfficio_ru_RU.ts b/client/ui/note-widget/NoteWidgetEfficio_ru_RU.ts similarity index 100% rename from ui/note-widget/NoteWidgetEfficio_ru_RU.ts rename to client/ui/note-widget/NoteWidgetEfficio_ru_RU.ts diff --git a/ui/note-widget/include/note_edit_dialog.h b/client/ui/note-widget/include/note_edit_dialog.h similarity index 64% rename from ui/note-widget/include/note_edit_dialog.h rename to client/ui/note-widget/include/note_edit_dialog.h index 46048a3..f4eba97 100644 --- a/ui/note-widget/include/note_edit_dialog.h +++ b/client/ui/note-widget/include/note_edit_dialog.h @@ -4,11 +4,11 @@ #include #include #include -#include "note.hpp" -#include "note_dao.hpp" +#include "client_implementation.h" +#include "model-proto/model.pb.h" #include "tags_dialog.h" -using namespace project_storage_model; +using Efficio_proto::Note; QT_BEGIN_NAMESPACE @@ -23,13 +23,14 @@ class NoteEditDialog final : public QDialog { public: explicit NoteEditDialog( - QWidget* parent = nullptr, - Note* note = new Note(0, "NULL", "NULL") + ClientImplementation *client, + QWidget *parent = nullptr, + Note *note = nullptr ); ~NoteEditDialog() override; - -private slots: - void on_save_button_click(); + static QString create_tag_style_sheet(int color_code); + private slots: + void on_save_button_click(); void on_join_button_click(); void on_add_members_button_click(); void on_add_tags_button_click(); @@ -40,19 +41,20 @@ private slots: void setup_connections(); void setup_ui(); - void add_member_avatar(const std::string& member); + void add_member_avatar(const std::string &member); void clear_member_avatars(); void update_tags_display(); - static QString create_tag_style_sheet(const QString& color); + [[nodiscard]] bool try_save_note() const; - Ui::NoteEditDialog* ui_{}; + Ui::NoteEditDialog *ui_{}; std::vector> member_avatars_; std::vector> tag_labels_; QList selected_tags_; - Note* note_; + Note *note_; + ClientImplementation *client_; }; -#endif // NOTE_EDIT_DIALOG_H \ No newline at end of file +#endif // NOTE_EDIT_DIALOG_H \ No newline at end of file diff --git a/ui/note-widget/include/note_edit_dialog_styles.h b/client/ui/note-widget/include/note_edit_dialog_styles.h similarity index 99% rename from ui/note-widget/include/note_edit_dialog_styles.h rename to client/ui/note-widget/include/note_edit_dialog_styles.h index 5ad1b63..147a083 100644 --- a/ui/note-widget/include/note_edit_dialog_styles.h +++ b/client/ui/note-widget/include/note_edit_dialog_styles.h @@ -4,7 +4,7 @@ #include namespace Ui { -QString light_theme = R"( +inline QString light_theme = R"( QDialog { background-color: #f5f5f5; } diff --git a/ui/note-widget/include/tags_dialog.h b/client/ui/note-widget/include/tags_dialog.h similarity index 89% rename from ui/note-widget/include/tags_dialog.h rename to client/ui/note-widget/include/tags_dialog.h index f8496c0..0033c4c 100644 --- a/ui/note-widget/include/tags_dialog.h +++ b/client/ui/note-widget/include/tags_dialog.h @@ -17,7 +17,7 @@ class TagsDialog final : public QDialog { struct Tag { bool is_checked; - QString color; + int color; QString name; }; @@ -27,6 +27,7 @@ class TagsDialog final : public QDialog { ); [[nodiscard]] QList get_selected_tags() const; + static QString get_color_by_code(int code); private: void setup_ui(); diff --git a/ui/note-widget/include/tags_dialog_styles.h b/client/ui/note-widget/include/tags_dialog_styles.h similarity index 100% rename from ui/note-widget/include/tags_dialog_styles.h rename to client/ui/note-widget/include/tags_dialog_styles.h diff --git a/ui/note-widget/src/note_edit_dialog.cpp b/client/ui/note-widget/src/note_edit_dialog.cpp similarity index 51% rename from ui/note-widget/src/note_edit_dialog.cpp rename to client/ui/note-widget/src/note_edit_dialog.cpp index ed18200..79078f7 100644 --- a/ui/note-widget/src/note_edit_dialog.cpp +++ b/client/ui/note-widget/src/note_edit_dialog.cpp @@ -1,4 +1,6 @@ #include "note_edit_dialog.h" +#include +#include #include #include #include @@ -6,15 +8,31 @@ #include #include #include -#include #include "./ui_note_edit_dialog.h" +#include "efficio-rpc-proto/efficio.grpc.pb.h" #include "note_edit_dialog_styles.h" #include "tags_dialog.h" +#include "update_requests.h" -NoteEditDialog::NoteEditDialog(QWidget* parent, Note* note) +using Efficio_proto::GetNoteRequest; +using Efficio_proto::GetNoteResponse; +using Efficio_proto::Update; +using grpc::Channel; +using grpc::ClientContext; +using grpc::Status; + +NoteEditDialog::NoteEditDialog( + ClientImplementation *client, + QWidget *parent, + Note *note +) : QDialog(parent), + client_(client), ui_(new Ui::NoteEditDialog), note_(note) { + if (note_ == nullptr) { + std::cerr << "Not a valid note!\n"; + } ui_->setupUi(this); setWindowTitle("EFFICIO"); @@ -29,13 +47,16 @@ NoteEditDialog::~NoteEditDialog() { } void NoteEditDialog::init_basic_fields() { - ui_->titleLineEdit->setText(QString::fromStdString(note_->get_title())); - ui_->descriptionTextEdit->setText(QString::fromStdString(note_->get_text())); + ui_->titleLineEdit->setMaxLength(50); + ui_->titleLineEdit->setText(QString::fromStdString(note_->title())); + ui_->descriptionTextEdit->setText(QString::fromStdString(note_->text())); } void NoteEditDialog::init_additional_fields() { - if (!note_->get_date().empty()) { - QDate date = QDate::fromString(QString::fromStdString(note_->get_date()), "yyyy-MM-dd"); + if (!note_->date().empty()) { + QDate date = QDate::fromString( + QString::fromStdString(note_->date()), "yyyy-MM-dd" + ); if (date.isValid()) { ui_->dateEdit->setDate(date); ui_->dateLabel->setVisible(true); @@ -43,21 +64,21 @@ void NoteEditDialog::init_additional_fields() { } } - if (!note_->get_members().empty()) { + if (!note_->members().empty()) { ui_->membersLabel->setVisible(true); - for (const auto& member : note_->get_members()) { + for (const auto &member : note_->members()) { add_member_avatar(member); } ui_->joinButton->setText("Покинуть"); } - if (!note_->get_tags().empty()) { + if (!note_->tags().empty()) { ui_->tagsLabel->setVisible(true); - for (const auto& tag : note_->get_tags()) { + for (const auto &tag : note_->tags()) { TagsDialog::Tag tag_info; tag_info.is_checked = true; - tag_info.color = QString::fromStdString(tag.color); - tag_info.name = QString::fromStdString(tag.name); + tag_info.color = tag.color(); + tag_info.name = QString::fromStdString(tag.text()); selected_tags_.append(tag_info); auto tag_label = std::make_unique(tag_info.name, this); @@ -69,16 +90,30 @@ void NoteEditDialog::init_additional_fields() { } void NoteEditDialog::setup_connections() { - connect(ui_->saveButton, &QPushButton::clicked, this, &NoteEditDialog::on_save_button_click); - connect(ui_->cancelButton, &QPushButton::clicked, this, &NoteEditDialog::reject); - connect(ui_->joinButton, &QPushButton::clicked, this, &NoteEditDialog::on_join_button_click); - connect(ui_->addMembersButton, &QPushButton::clicked, this, &NoteEditDialog::on_add_members_button_click); + connect( + ui_->saveButton, &QPushButton::clicked, this, + &NoteEditDialog::on_save_button_click + ); + connect( + ui_->cancelButton, &QPushButton::clicked, this, &NoteEditDialog::reject + ); + connect( + ui_->joinButton, &QPushButton::clicked, this, + &NoteEditDialog::on_join_button_click + ); + connect( + ui_->addMembersButton, &QPushButton::clicked, this, + &NoteEditDialog::on_add_members_button_click + ); connect(ui_->addDateButton, &QPushButton::clicked, this, [this]() { const bool is_visible = ui_->dateLabel->isVisible(); ui_->dateLabel->setVisible(!is_visible); ui_->dateEdit->setVisible(!is_visible); }); - connect(ui_->addTagsButton, &QPushButton::clicked, this, &NoteEditDialog::on_add_tags_button_click); + connect( + ui_->addTagsButton, &QPushButton::clicked, this, + &NoteEditDialog::on_add_tags_button_click + ); } void NoteEditDialog::setup_ui() { @@ -88,14 +123,21 @@ void NoteEditDialog::setup_ui() { } void NoteEditDialog::on_save_button_click() { - if (try_save_note()) { - QMessageBox::information(this, "Заметка сохранена", - QString("Заголовок: %1\nСодержимое: %2") - .arg(ui_->titleLineEdit->text(), ui_->descriptionTextEdit->toPlainText())); - } else { - QMessageBox::information(this, "Ошибка", "Не удалось сохранить заметку"); - } - close(); + try_save_note(); + // if () { \\ очень бесит когда много раз замтеку сохраняешь + // QMessageBox::information( + // this, "Заметка сохранена", + // QString("Заголовок: %1\nСодержимое: %2") + // .arg( + // ui_->titleLineEdit->text(), + // ui_->descriptionTextEdit->toPlainText() + // ) + // ); + // } else { + // QMessageBox::information( + // this, "Ошибка", "Не удалось сохранить заметку" + // ); + // } } void NoteEditDialog::on_join_button_click() { @@ -105,7 +147,7 @@ void NoteEditDialog::on_join_button_click() { if (!is_joined) { const std::string current_user = "TODO"; add_member_avatar(current_user); - note_->add_member(current_user); + note_->add_members(current_user); ui_->joinButton->setText("Покинуть"); } else { std::string current_user = "TODO"; @@ -114,7 +156,7 @@ void NoteEditDialog::on_join_button_click() { } } -void NoteEditDialog::add_member_avatar(const std::string& member) { +void NoteEditDialog::add_member_avatar(const std::string &member) { QPixmap pixmap(32, 32); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); @@ -130,7 +172,7 @@ void NoteEditDialog::add_member_avatar(const std::string& member) { } void NoteEditDialog::clear_member_avatars() { - for (auto& avatar : member_avatars_) { + for (auto &avatar : member_avatars_) { ui_->avatarsLayout->removeWidget(avatar.get()); } member_avatars_.clear(); @@ -149,34 +191,52 @@ void NoteEditDialog::on_add_tags_button_click() { } void NoteEditDialog::update_tags_display() { - for (auto& tag_label : tag_labels_) { + for (auto &tag_label : tag_labels_) { ui_->tagsLayout->removeWidget(tag_label.get()); } tag_labels_.clear(); ui_->tagsLabel->setVisible(!selected_tags_.empty()); - for (const auto &[is_checked, color, name] : selected_tags_) { + for (const auto &[is_checked, color_code, name] : selected_tags_) { if (is_checked) { auto tag_label = std::make_unique(name, this); - tag_label->setStyleSheet(create_tag_style_sheet(color)); + tag_label->setStyleSheet(create_tag_style_sheet(color_code)); ui_->tagsLayout->addWidget(tag_label.get()); tag_labels_.push_back(std::move(tag_label)); } } } -QString NoteEditDialog::create_tag_style_sheet(const QString& color) { +QString NoteEditDialog::create_tag_style_sheet(const int color_code) { return QString( - "background-color: %1; " - "color: white; " - "padding: 9px 10px; " - "border-radius: 5px; " - "font-family: 'Arial'; " - "font-size: 12px; " - "font-weight: bold;" - "width: 40px;" - "height: 25px;" - ).arg(color); + "background-color: %1; " + "color: white; " + "padding: 9px 10px; " + "border-radius: 5px; " + "font-family: 'Arial'; " + "font-size: 12px; " + "font-weight: bold;" + "width: 40px;" + "height: 25px;" + ) + .arg(TagsDialog::get_color_by_code(color_code)); +} + +Efficio_proto::Note_tag_colors color_code_to_note_tag_colors( + const int color_code +) { + switch (color_code) { + case 0: + return Efficio_proto::Note_tag_colors_Red; + case 1: + return Efficio_proto::Note_tag_colors_Blue; + case 2: + return Efficio_proto::Note_tag_colors_Pink; + case 3: + return Efficio_proto::Note_tag_colors_Green; + case 4: + return Efficio_proto::Note_tag_colors_Yellow; + } } bool NoteEditDialog::try_save_note() const { @@ -184,12 +244,17 @@ bool NoteEditDialog::try_save_note() const { note_->set_text(ui_->descriptionTextEdit->toPlainText().toStdString()); note_->set_date(ui_->dateEdit->date().toString("yyyy-MM-dd").toStdString()); + note_->clear_members(); + if (!member_avatars_.empty()) { + note_->add_members("TODO"); + } + note_->clear_tags(); - for (const auto &[is_checked, color, name] : selected_tags_) { - if (is_checked) { - note_->add_tag(name.toStdString(), color.toStdString()); - } + for (const auto &tag : selected_tags_) { + auto *new_tag = note_->add_tags(); + new_tag->set_text(tag.name.toStdString()); + new_tag->set_color(color_code_to_note_tag_colors(tag.color)); } - return NoteDao::update_note(*note_); + return client_->try_update_note(note_); } \ No newline at end of file diff --git a/ui/note-widget/src/tags_dialog.cpp b/client/ui/note-widget/src/tags_dialog.cpp similarity index 74% rename from ui/note-widget/src/tags_dialog.cpp rename to client/ui/note-widget/src/tags_dialog.cpp index 5a3d61e..89c6a7d 100644 --- a/ui/note-widget/src/tags_dialog.cpp +++ b/client/ui/note-widget/src/tags_dialog.cpp @@ -37,17 +37,18 @@ void TagsDialog::setup_ui() { tag_layout->addWidget(check_boxes_[i].get()); color_combo_boxes_[i] = std::make_unique(this); - color_combo_boxes_[i]->addItem("Красный", "#e7624b"); - color_combo_boxes_[i]->addItem("Синий", "#165d7b"); - color_combo_boxes_[i]->addItem("Розовый", "#bd6dab"); - color_combo_boxes_[i]->addItem("Зеленый", "#00b16b"); - color_combo_boxes_[i]->addItem("Желтый", "#e69f00"); + color_combo_boxes_[i]->addItem("Красный", 0); + color_combo_boxes_[i]->addItem("Синий", 1); + color_combo_boxes_[i]->addItem("Розовый", 2); + color_combo_boxes_[i]->addItem("Зеленый", 3); + color_combo_boxes_[i]->addItem("Желтый", 4); tag_layout->addWidget(color_combo_boxes_[i].get()); name_line_edits_[i] = std::make_unique(this); name_line_edits_[i]->setPlaceholderText( "Имя тега " + QString::number(i + 1) ); + name_line_edits_[i]->setMaxLength(15); tag_layout->addWidget(name_line_edits_[i].get()); main_layout->addLayout(tag_layout); @@ -78,10 +79,27 @@ QList TagsDialog::get_selected_tags() const { !name_line_edits_[i]->text().isEmpty()) { Tag tag; tag.is_checked = true; - tag.color = color_combo_boxes_[i]->currentData().toString(); + tag.color = color_combo_boxes_[i]->currentData().toInt(); tag.name = name_line_edits_[i]->text(); tags.append(tag); } } return tags; +} + +QString TagsDialog::get_color_by_code(const int code) { + switch (code) { + case 0: + return "#e7624b"; + case 1: + return "#165d7b"; + case 2: + return "#bd6dab"; + case 3: + return "#00b16b"; + case 4: + return "#e69f00"; + default: + return "ffffff"; + } } \ No newline at end of file diff --git a/ui/note-widget/ui/note_edit_dialog.ui b/client/ui/note-widget/ui/note_edit_dialog.ui similarity index 100% rename from ui/note-widget/ui/note_edit_dialog.ui rename to client/ui/note-widget/ui/note_edit_dialog.ui diff --git a/client/ui/theme-manager/CMakeLists.txt b/client/ui/theme-manager/CMakeLists.txt new file mode 100644 index 0000000..9f8bc1e --- /dev/null +++ b/client/ui/theme-manager/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.16) + +project(ThemeManager LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 COMPONENTS Core Gui Widgets Sql REQUIRED) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(SOURCES + src/theme_manager.cpp +) + +set(HEADERS + include/theme_manager.h +) + +add_library(ThemeManager STATIC ${SOURCES} ${HEADERS}) + +target_include_directories(ThemeManager + PUBLIC + $ + $ + $ +) + +target_link_libraries(ThemeManager PRIVATE Qt6::Widgets Qt6::Core Qt6::Gui Qt6::Sql Database model-proto) + diff --git a/client/ui/theme-manager/include/theme_manager.h b/client/ui/theme-manager/include/theme_manager.h new file mode 100644 index 0000000..5d014c9 --- /dev/null +++ b/client/ui/theme-manager/include/theme_manager.h @@ -0,0 +1,23 @@ +#ifndef THEME_MANAGER_H +#define THEME_MANAGER_H + +#include + +class ThemeManager final : public QObject { + Q_OBJECT + +public: + static ThemeManager *get_instance(); + void apply_theme(int theme); + [[nodiscard]] int get_current_theme() const; + +signals: + void on_theme_changed(int new_theme); + +private: + explicit ThemeManager(QObject *parent = nullptr); + static ThemeManager *instance_; + int current_theme_; +}; + +#endif // THEME_MANAGER_H \ No newline at end of file diff --git a/client/ui/theme-manager/src/theme_manager.cpp b/client/ui/theme-manager/src/theme_manager.cpp new file mode 100644 index 0000000..dc73868 --- /dev/null +++ b/client/ui/theme-manager/src/theme_manager.cpp @@ -0,0 +1,26 @@ +#include "theme_manager.h" +#include +#include + +ThemeManager *ThemeManager::instance_ = nullptr; + +ThemeManager::ThemeManager(QObject *parent) : QObject(parent) { + current_theme_ = 0; + emit on_theme_changed(this->current_theme_); +} + +ThemeManager *ThemeManager::get_instance() { + if (!instance_) { + instance_ = new ThemeManager(); + } + return instance_; +} + +void ThemeManager::apply_theme(const int theme) { + this->current_theme_ = theme; + emit on_theme_changed(this->current_theme_); +} + +int ThemeManager::get_current_theme() const { + return current_theme_; +} \ No newline at end of file diff --git a/database/include/database_manager.hpp b/database/include/database_manager.hpp deleted file mode 100644 index 1bba4b6..0000000 --- a/database/include/database_manager.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef DATABASE_MANAGER_HPP -#define DATABASE_MANAGER_HPP - -#include - -class DatabaseManager final { -public: - static DatabaseManager &get_instance(); - bool execute_query( - QSqlQuery &query, - const QString &query_str, - const QVariantList ¶ms = {} - ) const; - [[nodiscard]] QSqlDatabase get_database() const; - -private: - explicit DatabaseManager(); - ~DatabaseManager(); - QSqlDatabase database_; -}; - -#endif // DATABASE_MANAGER_HPP \ No newline at end of file diff --git a/database/include/lr_dao.hpp b/database/include/lr_dao.hpp deleted file mode 100644 index de8d5fb..0000000 --- a/database/include/lr_dao.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef LRDAO_H -#define LRDAO_H - -#include "database_manager.hpp" - -class LRDao { -public: - LRDao() = default; - static int try_register_user(const QString &login, const QString &password); - static bool validate_user(const QString &login, const QString &password); - static bool add_project_to_user(std::string user_login, int project_id); - static bool - get_user_projects(const std::string &login, std::vector &projects); - -private: - static QString hash_password(const QString &password); -}; - -#endif // LRDAO_H \ No newline at end of file diff --git a/database/include/note_dao.hpp b/database/include/note_dao.hpp deleted file mode 100644 index 0fbe980..0000000 --- a/database/include/note_dao.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef NOTEDAO_HPP -#define NOTEDAO_HPP - -#include -#include -#include "note.hpp" - -using namespace project_storage_model; - -class NoteDao { -public: - NoteDao() = default; - static bool initialize_note(int &id); - static bool update_note(const Note ¬e); - static bool delete_note(int id); - [[nodiscard]] static std::vector get_all_notes(); - [[nodiscard]] static Note get_note_by_id(int id); - -private: - static QString convert_string_set_to_postgres_array(const std::unordered_set& string_set); - static QString convert_tags_to_postgres_array(const std::vector& tags); -}; - -#endif // NOTEDAO_HPP \ No newline at end of file diff --git a/database/include/project_dao.hpp b/database/include/project_dao.hpp deleted file mode 100644 index a038af6..0000000 --- a/database/include/project_dao.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef PROJECT_DAO_HPP -#define PROJECT_DAO_HPP - -#include "database_manager.hpp" -#include "project.hpp" - -namespace DB { - -class ProjectDAO { -public: - ProjectDAO() = default; - static bool create_project(const std::string &name, int &id); - static bool get_project(int id, std::string &name, std::vector ¬es); - static bool add_note_to_project(int project_id, int note_id); -}; -} // namespace DB - -#endif \ No newline at end of file diff --git a/database/include/serialization.hpp b/database/include/serialization.hpp deleted file mode 100644 index f9b0def..0000000 --- a/database/include/serialization.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef SERIALIZATION_HPP -#define SERIALIZATION_HPP -#include "storage.hpp" - -class Serialization { -public: - static bool get_storage( - project_storage_model::Storage &storage, - const std::string &login - ); -}; - -#endif \ No newline at end of file diff --git a/database/src/database_manager.cpp b/database/src/database_manager.cpp deleted file mode 100644 index ad5b9ec..0000000 --- a/database/src/database_manager.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include "database_manager.hpp" -#include -#include -#include - -DatabaseManager::DatabaseManager() { - database_ = QSqlDatabase::addDatabase("QPSQL"); - database_.setHostName("localhost"); - database_.setPort(5432); - database_.setDatabaseName("efficio"); - database_.setUserName("efficio"); - database_.setPassword("admin"); - database_.open(); - - QSqlQuery query(database_); - query.exec( - "CREATE TABLE IF NOT EXISTS notes (" - "id SERIAL PRIMARY KEY, " - "title TEXT NOT NULL, " - "content TEXT, " - "members VARCHAR(50)[], " - "date VARCHAR(50), " - "tags VARCHAR(50)[]" - ")" - ); - - query.exec( - "CREATE TABLE IF NOT EXISTS users (" - "login VARCHAR(50) PRIMARY KEY, " - "password VARCHAR(50) NOT NULL, " - "projects INT[]" - ")" - ); - - query.exec( - "CREATE TABLE IF NOT EXISTS projects(" - "id SERIAL PRIMARY KEY, " - "name VARCHAR(50) NOT NULL, " - "notes INT[]" - ")" - ); -} - -DatabaseManager::~DatabaseManager() { - if (database_.isOpen()) { - database_.close(); - } -} - -DatabaseManager &DatabaseManager::get_instance() { - static DatabaseManager instance; - if (!instance.database_.isOpen() && !instance.database_.open()) { - throw std::runtime_error("Lost connection with database"); - } - return instance; -} - -bool DatabaseManager::execute_query( - QSqlQuery &query, - const QString &query_str, - const QVariantList ¶ms -) const { - QSqlQuery temp(database_); - query = std::move(temp); - - if (!query.prepare(query_str)) { - qDebug() << "Prepare error:" << query.lastError(); - return false; - } - - for (int i = 0; i < params.size(); i++) { - query.bindValue(i, params[i]); - } - - return query.exec(); -} - -QSqlDatabase DatabaseManager::get_database() const { - return database_; -} \ No newline at end of file diff --git a/database/src/lr_dao.cpp b/database/src/lr_dao.cpp deleted file mode 100644 index 3107f5b..0000000 --- a/database/src/lr_dao.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "lr_dao.hpp" -#include -#include -#include -#include -#include - -QString LRDao::hash_password(const QString &password) { - const QByteArray hash = - QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256); - return hash.toHex(); -} - -int LRDao::try_register_user(const QString &login, const QString &password) { - QSqlQuery query; - - const bool is_login_free = DatabaseManager::get_instance().execute_query( - query, "SELECT * FROM users WHERE login = ?", {login} - ); - - if (!is_login_free) { - return -1; - } - - QSqlQuery insert_query; - return DatabaseManager::get_instance().execute_query( - insert_query, "INSERT INTO users (login, password) VALUES (?, ?)", - {login, password} - ); -} - -bool LRDao::validate_user(const QString &login, const QString &password) { - QSqlQuery query; - - const QString query_str = - "SELECT * FROM users WHERE login = ? AND password = ?"; - const QVariantList params = {login, password}; - - const auto success = - DatabaseManager::get_instance().execute_query(query, query_str, params); - - return query.next() && success; -} - -bool LRDao::add_project_to_user(std::string user_login, int project_id) { - QSqlQuery query; - const QString query_str = - "UPDATE users SET projects = array_append(projects, ?)" - "WHERE login = ?"; - - const QVariantList params = { - QString::fromStdString(std::to_string(project_id)), - QString::fromStdString(user_login)}; - - return DatabaseManager::get_instance().execute_query( - query, query_str, params - ); -} - -bool LRDao::get_user_projects( - const std::string &login, - std::vector &projects -) { - QSqlQuery query; - const QString query_str = - "SELECT array_to_string(projects, ',') FROM users WHERE login = ?"; - - const QVariantList params = {QString::fromStdString(login)}; - const bool is_success = - DatabaseManager::get_instance().execute_query(query, query_str, params); - - if (is_success && query.next() && query.value(0).isValid()) { - QStringList ps = query.value(0).toString().split(","); - for (auto i : ps) { - projects.push_back(i.toInt()); - } - - return true; - } - return false; -} \ No newline at end of file diff --git a/database/src/note_dao.cpp b/database/src/note_dao.cpp deleted file mode 100644 index 369f192..0000000 --- a/database/src/note_dao.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include "note_dao.hpp" -#include -#include "database_manager.hpp" - -bool NoteDao::initialize_note(int &id) { - QSqlQuery query; - const auto is_successful = DatabaseManager::get_instance().execute_query( - query, - "INSERT INTO notes (title, content) " - "VALUES ('Пустая заметка', '')" - "RETURNING id" - ); - if (is_successful && query.next()) { - id = query.value(0).toInt(); - return true; - } - return false; -} - -bool NoteDao::update_note(const Note ¬e) { - QSqlQuery query; - - const QString sql_query = - "UPDATE notes SET " - "title = :title, " - "content = :content, " - "members = :members, " - "date = :date, " - "tags = :tags " - "WHERE id = :id"; - const QVariantList params = { - QString::fromStdString(note.get_title()), - QString::fromStdString(note.get_text()), - convert_string_set_to_postgres_array(note.get_members()), - QString::fromStdString(note.get_date()), - convert_tags_to_postgres_array(note.get_tags()), - note.get_id() - }; - - return DatabaseManager::get_instance().execute_query( - query, sql_query, params - ); -} - -bool NoteDao::delete_note(int id) { - QSqlQuery query; - const QString sql_query = "DELETE FROM notes WHERE id = :id"; - return DatabaseManager::get_instance().execute_query( - query, sql_query, {id} - ); -} - -std::vector NoteDao::get_all_notes() { - QSqlQuery query; - std::vector notes; - DatabaseManager::get_instance().execute_query(query, "SELECT * FROM notes"); - - while (query.next()) { - auto id = query.value("id").toUInt(); - auto title = query.value("title").toString().toStdString(); - auto text = query.value("content").toString().toStdString(); - notes.emplace_back(id, title, text); - } - - return notes; -} - -Note NoteDao::get_note_by_id(int id) { - QSqlQuery query; - DatabaseManager::get_instance().execute_query( - query, "SELECT title, content, array_to_string(tags, ','), date, array_to_string(members, ',') FROM notes WHERE id = ?", {id} - ); - query.next(); - auto title = query.value(0).toString().toStdString(); - auto text = query.value(1).toString().toStdString(); - auto date = query.value(3).toString().toStdString(); - Note result{id, title, text}; - auto tags_list = query.value(2).toString().split(","); - - if (!tags_list.empty() && tags_list[0] != "") { - for (const auto& tag_str : tags_list) { - - result.add_tag(tag_str.split(':')[0].toStdString(), tag_str.split(':')[1].toStdString()); - } - } - auto member_list = query.value(4).toString().split(","); - if (!member_list.empty() && member_list[0] != "") { - for (const auto& member : member_list) { - result.add_member(member.toStdString()); - } - } - - return result; -} - -QString NoteDao::convert_string_set_to_postgres_array(const std::unordered_set& string_set) { - if (string_set.empty()) { - return "{}"; - } - - QStringList buffer; - for (const auto& item : string_set) { - buffer << QString::fromStdString(item).replace(",", "\\,"); - } - return "{" + buffer.join(",") + "}"; -} - -QString NoteDao::convert_tags_to_postgres_array(const std::vector& tags) { - if (tags.empty()) { - return "{}"; - } - - QStringList buffer; - for (const auto &[name, color] : tags) { - buffer << QString("\"%1:%2\"") - .arg(QString::fromStdString(name).replace("\"", "\\\""), - QString::fromStdString(color).replace("\"", "\\\"")); - } - return "{" + buffer.join(",") + "}"; -} diff --git a/database/src/project_dao.cpp b/database/src/project_dao.cpp deleted file mode 100644 index 4370f4d..0000000 --- a/database/src/project_dao.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "project_dao.hpp" -#include -#include -#include -#include "database_manager.hpp" - -namespace DB { - -bool ProjectDAO::create_project(const std::string &name, int &id) { - // todo thread-safety - QSqlQuery query; - const QString query_str = - "INSERT INTO projects (name)" - " VALUES (:name)" - "RETURNING id"; - - const QVariantList params = {QString::fromStdString(name)}; - - bool is_successful = - DatabaseManager::get_instance().execute_query(query, query_str, params); - if (is_successful && query.next()) { - id = query.value(0).toInt(); - return true; - } - return false; -} - -bool ProjectDAO::get_project( - const int id, - std::string &name, - std::vector ¬es -) { - QSqlQuery query; - const QString query_str = - "SELECT name, array_to_string(notes, ',') FROM projects " - "WHERE id = ?"; - const QVariantList params = {id}; - const bool is_successful = - DatabaseManager::get_instance().execute_query(query, query_str, params); - - if (is_successful && query.next()) { - name = query.value("name").toString().toStdString(); - - for (const auto& i : query.value(1).toString().split(",")) { - notes.push_back(i.toInt()); - } - return true; - } - return false; -} - -bool ProjectDAO::add_note_to_project(const int project_id, const int note_id) { - QSqlQuery query; - const QString query_str = - "UPDATE projects " - "SET notes = array_append(notes, ?)" - "WHERE id = ?"; - const QVariantList params = {note_id, project_id}; - return DatabaseManager::get_instance().execute_query( - query, query_str, params - ); -} - -} // namespace DB \ No newline at end of file diff --git a/database/src/serialization.cpp b/database/src/serialization.cpp deleted file mode 100644 index 17e687e..0000000 --- a/database/src/serialization.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "serialization.hpp" -#include -#include -#include -#include "lr_dao.hpp" -#include "note.hpp" -#include "note_dao.hpp" -#include "project.hpp" -#include "project_dao.hpp" -#include "storage.hpp" - -bool Serialization::get_storage( - project_storage_model::Storage &storage, - const std::string &login -) { - std::vector projects; - if (LRDao::get_user_projects(login, projects)) { - for (auto p : projects) { - std::string project_name; - std::vector notes; - if (DB::ProjectDAO::get_project(p, project_name, notes)) { - Project project{p, project_name, ""}; - for (auto n : notes) { - if (n != 0) { - auto note = NoteDao::get_note_by_id(n); - project.add_note(note); - } - } - storage.add_project(Project(project)); - } else { - - return false; - } - } - } else { - - - return false; - } - return true; -} \ No newline at end of file diff --git a/proto/efficio-rpc-proto/efficio.proto b/proto/efficio-rpc-proto/efficio.proto index c5a38f6..a119033 100644 --- a/proto/efficio-rpc-proto/efficio.proto +++ b/proto/efficio-rpc-proto/efficio.proto @@ -5,16 +5,31 @@ import "model-proto/model.proto"; package Efficio_proto; service Update { + rpc UpdateNote(UpdateNoteRequest) returns (UpdateNoteResponse) {} rpc GetNote (GetNoteRequest) returns (GetNoteResponse) {} rpc GetProject (GetProjectRequest) returns (GetProjectResponse) {} rpc CreateNote (CreateNoteRequest) returns (CreateNoteResponse) {} rpc CreateProject (CreateProjectRequest) returns (CreateProjectResponse) {} rpc TryJoinProject (TryJoinProjectRequest) returns (TryJoinProjectResponse) {} + rpc TryLeaveProject (TryLeaveProjectRequest) returns (TryLeaveProjectResponse) {} + rpc UpdateProject (UpdateProjectRequest) returns (UpdateProjectResponse) {} } service Auth { rpc TryAuthenticateUser (AuthRequest) returns (AuthResponse) {} - rpc TryRegister1User (AuthRequest) returns (AuthResponse) {} + rpc TryRegisterUser (AuthRequest) returns (AuthResponse) {} +} + +message UpdateNoteRequest { + Note note = 1; +} + +message UpdateNoteResponse { + oneof response { + bool ok = 1; + Note note = 2; + string error_text = 3; + } } message GetNoteRequest { @@ -36,13 +51,14 @@ message GetProjectRequest { message GetProjectResponse { oneof response { Project project = 1; - string error_string = 2; + string error_text = 2; } } message CreateNoteRequest { User user = 1; optional string note_title = 2; // default = "Пустая заметка" + optional string project_code = 3; } message CreateNoteResponse { @@ -76,6 +92,24 @@ message TryJoinProjectResponse { } } +message TryLeaveProjectRequest { + User user = 1; + string code = 2; +} + +message TryLeaveProjectResponse { + int32 ok = 1; +} + +message UpdateProjectRequest { + Project project = 1; +} + +message UpdateProjectResponse { + bool ok = 1; + optional string error_text = 2; +} + message AuthRequest { User user = 1; } diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt deleted file mode 100644 index 73ccd8c..0000000 --- a/scripts/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -project(Scripts LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -file(GLOB SOURCES "src/*.cpp") -file(GLOB HEADERS "include/*.hpp") - -add_library(Scripts STATIC ${SOURCES} ${HEADERS}) - -target_include_directories(Scripts - PUBLIC - $ - $ -) \ No newline at end of file diff --git a/scripts/include/note.hpp b/scripts/include/note.hpp deleted file mode 100644 index e54e01a..0000000 --- a/scripts/include/note.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef NOTE_HPP -#define NOTE_HPP - -#include -#include -#include - -namespace project_storage_model { - -class Note { -public: - struct Tag { - std::string name; - std::string color; - }; - - Note(int id, std::string title, std::string text); - - [[nodiscard]] int get_id() const; - [[nodiscard]] const std::string &get_title() const; - [[nodiscard]] const std::string &get_text() const; - [[nodiscard]] const std::string &get_date() const; - [[nodiscard]] const std::vector &get_tags() const; - [[nodiscard]] const std::unordered_set &get_members() const; - - void set_title(const std::string &title); - void set_text(const std::string &text); - void add_tag(const std::string &tag, const std::string &color = "#e7624b"); - void set_date(const std::string &date); - void add_member(const std::string &member); - void remove_tag(const std::string &tag); - void clear_tags(); - void clear_members(); - -private: - int id_; - std::string title_; - std::string text_; - std::string date_; - std::vector tags_; - std::unordered_set members_; -}; - -} // namespace project_storage_model - -#endif \ No newline at end of file diff --git a/scripts/include/project.hpp b/scripts/include/project.hpp deleted file mode 100644 index ad83932..0000000 --- a/scripts/include/project.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef PROJECT_HPP -#define PROJECT_HPP - -#include -#include -#include "note.hpp" - -namespace project_storage_model { - -class Project { -public: - Project(int id, const std::string &name, const std::string &description); - - [[nodiscard]] int get_id() const; - [[nodiscard]] const std::string &get_name() const; - [[nodiscard]] const std::string &get_description() const; - const std::list &get_notes() const; - - Note &add_note(const Note ¬e); - void remove_note(int note_id); - void edit_description(const std::string &description); - -private: - int id_; - std::string name_; - std::string description_; - std::list notes_; -}; - -} // namespace project_storage_model - -#endif \ No newline at end of file diff --git a/scripts/include/storage.hpp b/scripts/include/storage.hpp deleted file mode 100644 index da6f948..0000000 --- a/scripts/include/storage.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef STORAGE_HPP -#define STORAGE_HPP - -#include -#include "project.hpp" -#include "user.hpp" - -namespace project_storage_model { - -class Storage { -public: - Project &add_project(const Project &project); - void remove_project(int project_id); - std::list &get_projects(); - - User &add_user(const User &user); - void remove_user(int user_id); - const std::list &get_users() const; - -private: - std::list projects_; - std::list users_; -}; - -} // namespace project_storage_model - -#endif \ No newline at end of file diff --git a/scripts/include/user.hpp b/scripts/include/user.hpp deleted file mode 100644 index 957fe63..0000000 --- a/scripts/include/user.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef USER_HPP -#define USER_HPP - -#include -#include -#include "project.hpp" - -namespace project_storage_model { - -class User { -public: - User(int id, const std::string &username); - - [[nodiscard]] int get_id() const; - [[nodiscard]] const std::string &get_username() const; - const std::list &get_projects() const; - - Project &add_project(const Project &project); - void remove_project(int project_id); - -private: - int id_; - std::string username_; - std::list projects_; -}; - -} // namespace project_storage_model - -#endif \ No newline at end of file diff --git a/scripts/src/note.cpp b/scripts/src/note.cpp deleted file mode 100644 index dcb8391..0000000 --- a/scripts/src/note.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "note.hpp" -#include -#include - -namespace project_storage_model { - -Note::Note(const int id, std::string title, std::string text) - : id_(id), title_(std::move(title)), text_(std::move(text)) { -} - -[[nodiscard]] int Note::get_id() const { - return id_; -} - -[[nodiscard]] const std::string &Note::get_title() const { - return title_; -} - -[[nodiscard]] const std::string &Note::get_text() const { - return text_; -} - -const std::string &Note::get_date() const { - return date_; -} - -const std::vector &Note::get_tags() const { - return tags_; -} - -const std::unordered_set &Note::get_members() const { - return members_; -} - -void Note::set_title(const std::string &title) { - title_ = title; -} - -void Note::set_text(const std::string &text) { - text_ = text; -} - -void Note::add_tag(const std::string &tag, const std::string &color) { - tags_.push_back({tag, color}); -} - -void Note::set_date(const std::string &date) { - date_ = date; -} - -void Note::add_member(const std::string &member) { - members_.insert(member); -} - -void Note::remove_tag(const std::string &tag) { - tags_.erase(std::remove_if(tags_.begin(), tags_.end(), - [&tag](const Tag& t) { return t.name == tag; }), tags_.end()); -} - -void Note::clear_tags() { - tags_.clear(); -} - -void Note::clear_members() { - members_.clear(); -} - -} // namespace project_storage_model \ No newline at end of file diff --git a/scripts/src/project.cpp b/scripts/src/project.cpp deleted file mode 100644 index a55e555..0000000 --- a/scripts/src/project.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "../include/project.hpp" -#include - -namespace project_storage_model { - -Project::Project( - int id, - const std::string &name, - const std::string &description -) - : id_(std::move(id)), - name_(std::move(name)), - description_(std::move(description)) { -} - -[[nodiscard]] int Project::get_id() const { - return id_; -} - -[[nodiscard]] const std::string &Project::get_name() const { - return name_; -} - -[[nodiscard]] const std::string &Project::get_description() const { - return description_; -} - -const std::list &Project::get_notes() const { - return notes_; -} - -Note &Project::add_note(const Note ¬e) { - notes_.push_back(note); - return notes_.back(); -} - -void Project::remove_note(int note_id) { - notes_.erase( - std::remove_if( - notes_.begin(), notes_.end(), - [note_id](const Note ¬e) { return note.get_id() == note_id; } - ), - notes_.end() - ); -} - -void Project::edit_description(const std::string &description) { - this->description_ = description; -} - -} // namespace project_storage_model diff --git a/scripts/src/storage.cpp b/scripts/src/storage.cpp deleted file mode 100644 index e452e6b..0000000 --- a/scripts/src/storage.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "../include/storage.hpp" -#include - -namespace project_storage_model { - -Project &Storage::add_project(const Project &project) { - projects_.push_back(project); - return projects_.back(); -} - -void Storage::remove_project(int project_id) { - projects_.erase( - std::remove_if( - projects_.begin(), projects_.end(), - [project_id](const Project &project) { - return project.get_id() == project_id; - } - ), - projects_.end() - ); -} - -std::list &Storage::get_projects() { - return projects_; -} - -User &Storage::add_user(const User &user) { - users_.push_back(user); - return users_.back(); -} - -void Storage::remove_user(int user_id) { - users_.erase( - std::remove_if( - users_.begin(), users_.end(), - [user_id](const User &user) { return user.get_id() == user_id; } - ), - users_.end() - ); -} - -const std::list &Storage::get_users() const { - return users_; -} - -} // namespace project_storage_model diff --git a/scripts/src/user.cpp b/scripts/src/user.cpp deleted file mode 100644 index e1cbb5f..0000000 --- a/scripts/src/user.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "../include/user.hpp" -#include - -namespace project_storage_model { - -User::User(int id, const std::string &username) - : id_(std::move(id)), username_(std::move(username)) { -} - -[[nodiscard]] int User::get_id() const { - return id_; -} - -[[nodiscard]] const std::string &User::get_username() const { - return username_; -} - -const std::list &User::get_projects() const { - return projects_; -} - -Project &User::add_project(const Project &project) { - projects_.push_back(project); - return projects_.back(); -} - -void User::remove_project(int project_id) { - projects_.erase( - std::remove_if( - projects_.begin(), projects_.end(), - [project_id](const Project &project) { - return project.get_id() == project_id; - } - ), - projects_.end() - ); -} - -} // namespace project_storage_model diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000..a62b1f2 --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) +project(Server) + +add_subdirectory(Service) +add_subdirectory(database) +add_subdirectory(Handlers) + +add_executable(Server main.cpp) + +target_link_libraries(Server Service) \ No newline at end of file diff --git a/server/Handlers/CMakeLists.txt b/server/Handlers/CMakeLists.txt new file mode 100644 index 0000000..b14cf6d --- /dev/null +++ b/server/Handlers/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) + +project(Handlers LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +file(GLOB SOURCES "src/*.cpp") +file(GLOB HEADERS "include/*.hpp") + +add_library(Handlers STATIC ${SOURCES} ${HEADERS}) + +target_link_libraries(Handlers + efficio-rpc + model-proto + Database +) + +target_include_directories(${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_BINARY_DIR} +) \ No newline at end of file diff --git a/server/Handlers/include/update_handler.hpp b/server/Handlers/include/update_handler.hpp new file mode 100644 index 0000000..befe658 --- /dev/null +++ b/server/Handlers/include/update_handler.hpp @@ -0,0 +1,40 @@ +#ifndef UPDATE_HANDLER_H +#define UPDATE_HANDLER_H + +#include +#include + +using Efficio_proto::CreateProjectRequest; +using Efficio_proto::CreateProjectResponse; +using Efficio_proto::GetProjectRequest; +using Efficio_proto::GetProjectResponse; +using Efficio_proto::Project; +using Efficio_proto::TryJoinProjectRequest; +using Efficio_proto::TryJoinProjectResponse; +using Efficio_proto::TryLeaveProjectRequest; +using Efficio_proto::TryLeaveProjectResponse; +using Efficio_proto::UpdateProjectRequest; +using Efficio_proto::UpdateProjectResponse; + +class UpdateHandler { +public: + static bool create_project( + const CreateProjectRequest &request, + CreateProjectResponse &response + ); + static bool try_join_project( + const TryJoinProjectRequest &request, + TryJoinProjectResponse &response + ); + static bool try_leave_project( + const TryLeaveProjectRequest &request, + TryLeaveProjectResponse &response + ); + static bool + get_project(const GetProjectRequest &request, GetProjectResponse &response); + +private: + static std::string generate_project_code(); +}; + +#endif \ No newline at end of file diff --git a/server/Handlers/src/update_handler.cpp b/server/Handlers/src/update_handler.cpp new file mode 100644 index 0000000..53898d3 --- /dev/null +++ b/server/Handlers/src/update_handler.cpp @@ -0,0 +1,125 @@ +#include "update_handler.hpp" +#include +#include +#include "lr_dao.hpp" +#include "project_dao.hpp" + +using Efficio_proto::Project; + +std::string UpdateHandler::generate_project_code() { + static const std::string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + static std::mt19937 gen(std::random_device{}()); + static std::uniform_int_distribution dis(0, chars.size() - 1); + std::string result; + for (size_t i = 0; i < 6; ++i) { + result += chars[dis(gen)]; + } + return result; +} + +bool UpdateHandler::create_project( + const CreateProjectRequest &request, + CreateProjectResponse &response +) { + if (!LRDao::validate_user( + request.user().login(), request.user().hashed_password() + )) { + response.set_allocated_error_text(new std::string("Incorrect user")); + return false; + } + Project *project = new Project(); + project->set_title(request.project_title()); + std::string project_code; + while (true) { + project_code = generate_project_code(); + if (ProjectDAO::code_available(project_code)) { + break; + } + } + project->set_code(project_code); + *project->add_members() = request.user().login(); + ProjectDAO::insert_project(*project); + LRDao::add_project_to_user(request.user().login(), project_code); + response.set_allocated_project(project); + return true; +} + +bool UpdateHandler::try_join_project( + const TryJoinProjectRequest &request, + TryJoinProjectResponse &response +) { + if (!LRDao::validate_user( + request.user().login(), request.user().hashed_password() + )) { + response.set_allocated_error_text(new std::string("Incorrect user")); + return false; + } + + { + std::vector project_codes; + LRDao::get_user_projects(request.user().login(), project_codes); + for (const auto &project_code : project_codes) { + if (project_code == request.code()) { + response.set_allocated_error_text( + new std::string("Already have this project") + ); + return false; + } + } + } + + if (!ProjectDAO::add_member_to_project( + request.code(), request.user().login() + )) { + response.set_allocated_error_text( + new std::string("Can't add member to project") + ); + return false; + } + if (!LRDao::add_project_to_user(request.user().login(), request.code())) { + response.set_allocated_error_text( + new std::string("Can't add project to user") + ); + return false; + } + + Project *project = new Project(); + ProjectDAO::get_project(request.code(), *project); + response.set_allocated_project(project); + + return true; +} + +bool UpdateHandler::try_leave_project( + const TryLeaveProjectRequest &request, + TryLeaveProjectResponse &response +) { + if (!LRDao::validate_user( + request.user().login(), request.user().hashed_password() + )) { + return false; + } + + ProjectDAO::delete_member_from_project( + request.code(), request.user().login() + ); + + LRDao::delete_project_from_user(request.user().login(), request.code()); + + return true; +} + +bool UpdateHandler::get_project( + const GetProjectRequest &request, + GetProjectResponse &response +) { + + Project *project = new Project(); + bool ok = ProjectDAO::get_project(request.code(), *project); + if (!ok) { + response.set_allocated_error_text(new std::string("Can't get project")); + return false; + } + response.set_allocated_project(project); + return true; +} diff --git a/server/Service/CMakeLists.txt b/server/Service/CMakeLists.txt new file mode 100644 index 0000000..811dc41 --- /dev/null +++ b/server/Service/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.16) + +project(Service LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(libpqxx REQUIRED) +find_package(Protobuf CONFIG REQUIRED) +find_package(gRPC CONFIG REQUIRED) +find_package(Threads) + + +file(GLOB SOURCES "src/*.cpp") +file(GLOB HEADERS "include/*.h") + +add_library(Service STATIC ${SOURCES} ${HEADERS} + include/auth_service.h + src/auth_service.cpp) + +target_link_libraries(Service + NoteWidget + Database + model-proto + efficio-rpc + protobuf::libprotobuf + Handlers + gRPC::grpc + gRPC::grpc++ + pqxx +) + +target_include_directories(${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_BINARY_DIR} +) \ No newline at end of file diff --git a/server/Service/include/auth_service.h b/server/Service/include/auth_service.h new file mode 100644 index 0000000..c38c08a --- /dev/null +++ b/server/Service/include/auth_service.h @@ -0,0 +1,67 @@ +#ifndef AUTH_SERVICE_H +#define AUTH_SERVICE_H + +#include +#include +#include +#include "common_server_call.h" + +using grpc::ServerAsyncResponseWriter; +using grpc::ServerContext; + +using Efficio_proto::Auth; + +using Efficio_proto::AuthRequest; +using Efficio_proto::AuthResponse; + +class AuthService final { + Auth::AsyncService service_; + ServerContext ctx_; + ServerCompletionQueue* cq_; + std::unique_ptr server_; + + class AuthServerOperation : public CommonServerCall { + protected: + AuthRequest request_; + ServerAsyncResponseWriter responder_; + Auth::AsyncService *service_; + + explicit AuthServerOperation( + Auth::AsyncService *service, + ServerCompletionQueue *cq + ) + : CommonServerCall(cq), responder_(&ctx_), service_(service) { + } + + public: + ~AuthServerOperation() override = default; + void Proceed(bool ok) override = 0; + }; + +public: + class TryAuthenticateUserServerCall final : public AuthServerOperation { + public: + explicit TryAuthenticateUserServerCall( + Auth::AsyncService *service, + ServerCompletionQueue *cq + ); + void Proceed(bool) override; + }; + + class TryRegisterUserServerCall final : AuthServerOperation { + public: + explicit TryRegisterUserServerCall( + Auth::AsyncService *service, + ServerCompletionQueue *cq + ); + void Proceed(bool) override; + }; + + Auth::AsyncService &get_service(); + + explicit AuthService(ServerCompletionQueue* cq); + + void run(); +}; + +#endif // AUTH_SERVICE_H diff --git a/server/Service/include/common_server_call.h b/server/Service/include/common_server_call.h new file mode 100644 index 0000000..a5f4fe8 --- /dev/null +++ b/server/Service/include/common_server_call.h @@ -0,0 +1,27 @@ +#ifndef CALL_DATA_H +#define CALL_DATA_H +#include + +using grpc::ServerCompletionQueue; +using grpc::ServerContext; + +class CommonServerCall { +public: + ServerCompletionQueue *cq_; + ServerContext ctx_; + + enum CallStatus { CREATE, PROCESS, FINISH }; + + CallStatus status_; + +public: + explicit CommonServerCall(ServerCompletionQueue *cq) + : cq_(cq), status_(CREATE) { + } + + virtual ~CommonServerCall() = default; + + virtual void Proceed(bool = true) = 0; +}; + +#endif \ No newline at end of file diff --git a/server/Service/include/server_implementation.h b/server/Service/include/server_implementation.h new file mode 100644 index 0000000..9ccccf1 --- /dev/null +++ b/server/Service/include/server_implementation.h @@ -0,0 +1,25 @@ +#ifndef SERVERIMPLEMENTATION_H +#define SERVERIMPLEMENTATION_H + +#include +#include "auth_service.h" +#include "update_service.h" + +using grpc::Server; +using grpc::ServerCompletionQueue; + +class ServerImplementation final { + std::unique_ptr cq_; + std::unique_ptr server_; + UpdateService update_service_; + AuthService auth_service_; + +public: + explicit ServerImplementation( + const uint16_t port, + grpc::ServerBuilder &builder + ); + void HandleRPCs() const; +}; + +#endif // SERVERIMPLEMENTATION_H diff --git a/server/Service/include/update_service.h b/server/Service/include/update_service.h new file mode 100644 index 0000000..589b16b --- /dev/null +++ b/server/Service/include/update_service.h @@ -0,0 +1,137 @@ +#ifndef UPDATE_SERVICE_H +#define UPDATE_SERVICE_H + +#include +#include +#include +#include "common_server_call.h" + +using grpc::ServerAsyncResponseWriter; +using grpc::ServerContext; + +using Efficio_proto::CreateNoteRequest; +using Efficio_proto::CreateNoteResponse; +using Efficio_proto::CreateProjectRequest; +using Efficio_proto::CreateProjectResponse; +using Efficio_proto::GetNoteRequest; +using Efficio_proto::GetNoteResponse; +using Efficio_proto::GetProjectRequest; +using Efficio_proto::GetProjectResponse; +using Efficio_proto::TryJoinProjectRequest; +using Efficio_proto::TryJoinProjectResponse; +using Efficio_proto::TryLeaveProjectRequest; +using Efficio_proto::TryLeaveProjectResponse; +using Efficio_proto::Update; +using Efficio_proto::UpdateNoteRequest; +using Efficio_proto::UpdateNoteResponse; + +class UpdateService final { + Update::AsyncService service_; + ServerContext ctx_; + ServerCompletionQueue *cq_; + std::unique_ptr server_; + +public: + UpdateService(ServerCompletionQueue *cq); + + class UpdateNoteServerCall final : public CommonServerCall { + UpdateNoteRequest request_; + ServerAsyncResponseWriter responder_; + Update::AsyncService *service_; + + public: + explicit UpdateNoteServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq + ); + void Proceed(bool ok) override; + }; + + class GetNoteServerCall final : public CommonServerCall { + GetNoteRequest request_; + ServerAsyncResponseWriter responder_; + Update::AsyncService *service_; + + public: + explicit GetNoteServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq + ); + void Proceed(bool ok) override; + }; + + class GetProjectServerCall final : public CommonServerCall { + GetProjectRequest request_; + GetProjectResponse response_; + ServerAsyncResponseWriter responder_; + Update::AsyncService *service_; + + public: + explicit GetProjectServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq + ); + void Proceed(bool) override; + }; + + class CreateProjectServerCall : public CommonServerCall { + CreateProjectRequest request_; + CreateProjectResponse response_; + ServerAsyncResponseWriter responder_; + Update::AsyncService *service_; + + public: + explicit CreateProjectServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq + ); + void Proceed(bool) override; + }; + + class TryJoinProjectServerCall : public CommonServerCall { + TryJoinProjectRequest request_; + TryJoinProjectResponse response_; + ServerAsyncResponseWriter responder_; + Update::AsyncService *service_; + + public: + explicit TryJoinProjectServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq + ); + void Proceed(bool) override; + }; + + class TryLeaveProjectServerCall : public CommonServerCall { + TryLeaveProjectRequest request_; + TryLeaveProjectResponse response_; + ServerAsyncResponseWriter responder_; + Update::AsyncService *service_; + + public: + explicit TryLeaveProjectServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq + ); + void Proceed(bool) override; + }; + + class CreateNoteServerCall final : public CommonServerCall { + CreateNoteRequest request_; + ServerAsyncResponseWriter responder_; + Update::AsyncService *service_; + + public: + explicit CreateNoteServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq + ); + void Proceed(bool ok) override; + }; + + Update::AsyncService &get_service(); + + void run(); +}; + +#endif // UPDATE_SERVICE_H diff --git a/server/Service/src/auth_service.cpp b/server/Service/src/auth_service.cpp new file mode 100644 index 0000000..ece80d6 --- /dev/null +++ b/server/Service/src/auth_service.cpp @@ -0,0 +1,135 @@ +#include "auth_service.h" +#include "lr_dao.hpp" +#include "project_dao.hpp" + +using Efficio_proto::Storage; + +AuthService::AuthService(ServerCompletionQueue *cq) : cq_(cq) { +} + +AuthService::TryAuthenticateUserServerCall::TryAuthenticateUserServerCall( + Auth::AsyncService *service, + ServerCompletionQueue *cq +) + : AuthServerOperation(service, cq) { + service_->RequestTryAuthenticateUser( + &ctx_, &request_, &responder_, cq_, cq_, this + ); + status_ = PROCESS; +} + +void AuthService::TryAuthenticateUserServerCall::Proceed(const bool ok) { + if (!ok) { + delete this; + return; + } + + switch (status_) { + case PROCESS: { + std::cout << "[SERVER]: GOT AUTH REQUEST\n"; + new TryAuthenticateUserServerCall(service_, cq_); + + AuthResponse response; + + const int query_exit_code = LRDao::validate_user( + request_.user().login(), request_.user().hashed_password() + ); + + if (query_exit_code == 1) { + response.mutable_user()->CopyFrom(request_.user()); + std::cout << "[SERVER]: WELCOME, " + << response.mutable_user()->login() << "\n"; + + Storage user_storage; + bool have_projects = ProjectDAO::get_all_user_projects( + request_.user().login(), user_storage + ); + + if (have_projects) { + response.mutable_user()->mutable_storage()->CopyFrom( + user_storage + ); + std::cout << "[SERVER]: DOWNLOADED YOUR PROJECT: " + << response.user().storage().projects()[0].code() + << "\n"; + } else { + std::cout << "[SERVER]: YOU DONT HAVE ANY PROJECTS\n"; + } + + } else { + std::cout << "[SERVER]: SQL QUERY ERROR\n"; + response.set_error_text( + "[SERVER ERROR]: Не удалось выполнить запрос в базу данных " + "на проверку " + "корректности логина и пароля" + ); + } + + status_ = FINISH; + responder_.Finish(response, grpc::Status::OK, this); + break; + } + case FINISH: { + delete this; + break; + } + } +} + +AuthService::TryRegisterUserServerCall::TryRegisterUserServerCall( + Auth::AsyncService *service, + ServerCompletionQueue *cq +) + : AuthServerOperation(service, cq) { + service_->RequestTryRegisterUser( + &ctx_, &request_, &responder_, cq_, cq_, this + ); + status_ = PROCESS; +} + +void AuthService::TryRegisterUserServerCall::Proceed(const bool ok) { + if (!ok) { + delete this; + return; + } + + switch (status_) { + case PROCESS: { + std::cout << "[SERVER]: GOT REGISTER REQUEST\n"; + new TryRegisterUserServerCall(service_, cq_); + + AuthResponse response; + + const int query_exit_code = LRDao::try_register_user( + request_.user().login(), request_.user().hashed_password() + ); + + if (query_exit_code < 1) { + response.set_error_text( + "[SERVER ERROR]: Не удалось выполнить запрос на запись " + "нового пользователя " + "в базу данных" + ); + } else { + response.mutable_user()->CopyFrom(request_.user()); + } + + status_ = FINISH; + responder_.Finish(response, grpc::Status::OK, this); + break; + } + case FINISH: { + delete this; + break; + } + } +} + +Auth::AsyncService &AuthService::get_service() { + return service_; +} + +void AuthService::run() { + new TryAuthenticateUserServerCall(&service_, cq_); + new TryRegisterUserServerCall(&service_, cq_); +} \ No newline at end of file diff --git a/server/Service/src/server_implementation.cpp b/server/Service/src/server_implementation.cpp new file mode 100644 index 0000000..ba2f5c2 --- /dev/null +++ b/server/Service/src/server_implementation.cpp @@ -0,0 +1,30 @@ +#include "server_implementation.h" +#include "common_server_call.h" + +ServerImplementation::ServerImplementation( + const uint16_t port, + grpc::ServerBuilder &builder +) + : cq_(builder.AddCompletionQueue()), + update_service_(cq_.get()), + auth_service_(cq_.get()) { + builder.AddListeningPort( + "127.0.0.1:" + std::to_string(port), grpc::InsecureServerCredentials() + ); + + builder.RegisterService(&update_service_.get_service()); + builder.RegisterService(&auth_service_.get_service()); + + server_ = builder.BuildAndStart(); + + update_service_.run(); + auth_service_.run(); +} + +void ServerImplementation::HandleRPCs() const { + void *tag; + bool ok; + while (cq_->Next(&tag, &ok)) { + static_cast(tag)->Proceed(ok); + } +} diff --git a/server/Service/src/update_service.cpp b/server/Service/src/update_service.cpp new file mode 100644 index 0000000..18e66af --- /dev/null +++ b/server/Service/src/update_service.cpp @@ -0,0 +1,326 @@ +#include "update_service.h" +#include "common_server_call.h" +#include "project_dao.hpp" +#include "update_handler.hpp" + +using grpc::ServerAsyncResponseWriter; + +using Efficio_proto::GetNoteRequest; +using Efficio_proto::GetNoteResponse; +using Efficio_proto::Project; + +UpdateService::UpdateService(ServerCompletionQueue *cq) : cq_(cq) { +} + +void UpdateService::run() { + new GetProjectServerCall(&service_, cq_); + new CreateProjectServerCall(&service_, cq_); + new TryJoinProjectServerCall(&service_, cq_); + new TryLeaveProjectServerCall(&service_, cq_); + + new GetNoteServerCall(&service_, cq_); + new CreateNoteServerCall(&service_, cq_); + new UpdateNoteServerCall(&service_, cq_); +} + +UpdateService::UpdateNoteServerCall::UpdateNoteServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq +) + : CommonServerCall(cq), responder_(&ctx_), service_(service) { + service_->RequestUpdateNote(&ctx_, &request_, &responder_, cq_, cq_, this); + status_ = PROCESS; +} + +void UpdateService::UpdateNoteServerCall::Proceed(const bool ok) { + if (!ok) { + delete this; + return; + } + + switch (status_) { + case PROCESS: { + new UpdateNoteServerCall(service_, cq_); + + UpdateNoteResponse response; + + NoteDao::update_note(request_.note()); + response.mutable_note()->CopyFrom(request_.note()); + std::cout << "[SERVER]: UPDATE NOTE REQUEST id=" + << request_.note().id() + << ", title=" << response.mutable_note()->title() + << std::endl; + responder_.Finish(response, grpc::Status::OK, this); + status_ = FINISH; + break; + } + case FINISH: { + delete this; + break; + } + } +} + +UpdateService::GetNoteServerCall::GetNoteServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq +) + : CommonServerCall(cq), responder_(&ctx_), service_(service) { + service_->RequestGetNote(&ctx_, &request_, &responder_, cq_, cq_, this); + status_ = PROCESS; +} + +void UpdateService::GetNoteServerCall::Proceed(const bool ok) { + if (!ok) { + delete this; + return; + } + + switch (status_) { + case PROCESS: { + new GetNoteServerCall(service_, cq_); + + GetNoteResponse response; + + const auto note = NoteDao::get_note(request_.id()); + response.mutable_note()->CopyFrom(note); + + std::cout << "[SERVER]: FETCH NOTE REQUEST id=" << request_.id() + << ", title=" << response.note().title(); + + if (response.note().tags_size() > 0) { + std::cout << ", first tag=" << response.note().tags()[0].text() + << ":" << response.note().tags()[0].color(); + } else { + std::cout << ", no tags"; + } + std::cout << std::endl; + + responder_.Finish(response, grpc::Status::OK, this); + status_ = FINISH; + break; + } + case FINISH: { + delete this; + break; + } + } +} + +UpdateService::GetProjectServerCall::GetProjectServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq +) + : CommonServerCall(cq), responder_(&ctx_), service_(service) { + this->Proceed(true); +} + +void UpdateService::GetProjectServerCall::Proceed(const bool ok = true) { + switch (status_) { + case CREATE: { + status_ = PROCESS; + service_->RequestGetProject( + &ctx_, &request_, &responder_, cq_, cq_, this + ); + std::cout << "[SERVER] : {get project} : start listening" + << std::endl; + break; + } + case PROCESS: { + new GetProjectServerCall(service_, cq_); + status_ = FINISH; + + std::cout << "[SERVER] : {get project} : get request, code=" + << request_.code() << std::endl; + + UpdateHandler::get_project(request_, response_); + responder_.Finish(response_, grpc::Status::OK, this); + break; + } + case FINISH: { + std::cout << "[SERVER] : {get project} : deleting call" + << std::endl; + delete this; + } + } +} + +UpdateService::CreateProjectServerCall::CreateProjectServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq +) + : CommonServerCall(cq), responder_(&ctx_), service_(service) { + this->Proceed(true); +} + +void UpdateService::CreateProjectServerCall::Proceed(const bool ok) { + if (!ok) { + delete this; + return; + } + switch (status_) { + case CREATE: { + status_ = PROCESS; + service_->RequestCreateProject( + &ctx_, &request_, &responder_, cq_, cq_, this + ); + std::cout << "[SERVER] : {create project} : start listening" + << std::endl; + break; + } + case PROCESS: { + status_ = FINISH; + new CreateProjectServerCall(service_, cq_); + + std::cout << "[SERVER] : {create project} : get request, title=" + << request_.project_title() << std::endl; + + UpdateHandler::create_project(request_, response_); + + responder_.Finish(response_, grpc::Status::OK, this); + break; + } + case FINISH: { + std::cout << "[SERVER] : {create project} : deleting call" + << std::endl; + delete this; + } + } +} + +UpdateService::TryJoinProjectServerCall::TryJoinProjectServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq +) + : CommonServerCall(cq), responder_(&ctx_), service_(service) { + this->Proceed(true); +} + +void UpdateService::TryJoinProjectServerCall::Proceed(const bool ok = true) { + if (!ok) { + delete this; + return; + } + switch (status_) { + case CREATE: { + status_ = PROCESS; + service_->RequestTryJoinProject( + &ctx_, &request_, &responder_, cq_, cq_, this + ); + std::cout << "[SERVER] : {try join project} : start listening" + << std::endl; + break; + } + case PROCESS: { + status_ = FINISH; + new TryJoinProjectServerCall(service_, cq_); + std::cout << "[SERVER] : {try join project} : get request, code=" + << request_.code() << std::endl; + + UpdateHandler::try_join_project(request_, response_); + + responder_.Finish(response_, grpc::Status::OK, this); + break; + } + case FINISH: { + std::cout << "[SERVER] : {try join project} : deleting call" + << std::endl; + delete this; + } + } +} + +UpdateService::TryLeaveProjectServerCall::TryLeaveProjectServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq +) + : CommonServerCall(cq), responder_(&ctx_), service_(service) { + this->Proceed(true); +} + +void UpdateService::TryLeaveProjectServerCall::Proceed(const bool ok) { + if (!ok) { + delete this; + return; + } + switch (status_) { + case CREATE: { + status_ = PROCESS; + service_->RequestTryLeaveProject( + &ctx_, &request_, &responder_, cq_, cq_, this + ); + std::cout << "[SERVER] : {try leave project} : start listening" + << std::endl; + break; + } + case PROCESS: { + status_ = FINISH; + new TryLeaveProjectServerCall(service_, cq_); + std::cout << "[SERVER] : {try leave project} : get request, code=" + << request_.code() << std::endl; + + UpdateHandler::try_leave_project(request_, response_); + + response_.set_ok(1); + responder_.Finish(response_, grpc::Status::OK, this); + } + case FINISH: { + std::cout << "[SERVER] : {try leave project} : deleting call" + << std::endl; + delete this; + } + } +} + +UpdateService::CreateNoteServerCall::CreateNoteServerCall( + Update::AsyncService *service, + ServerCompletionQueue *cq +) + : CommonServerCall(cq), responder_(&ctx_), service_(service) { + this->Proceed(true); +} + +void UpdateService::CreateNoteServerCall::Proceed(const bool ok) { + if (!ok) { + delete this; + return; + } + + switch (status_) { + case CREATE: { + status_ = PROCESS; + service_->RequestCreateNote( + &ctx_, &request_, &responder_, cq_, cq_, this + ); + std::cout << "[SERVER] : {create note} : start listening" + << std::endl; + break; + } + case PROCESS: { + status_ = FINISH; + new CreateNoteServerCall(service_, cq_); + + std::cout << "[SERVER] : {create note} : get request, title=" + << request_.note_title() << std::endl; + + CreateNoteResponse response; + const auto created_note = + NoteDao::initialize_note_for_user(request_.user().login()); + ProjectDAO::add_note_to_project(request_.project_code(), created_note.id()); + response.mutable_note()->CopyFrom(created_note); + + responder_.Finish(response, grpc::Status::OK, this); + break; + } + case FINISH: { + std::cout << "[SERVER] : {create note} : deleting call" + << std::endl; + delete this; + break; + } + } +} + +Update::AsyncService &UpdateService::get_service() { + return service_; +} \ No newline at end of file diff --git a/database/CMakeLists.txt b/server/database/CMakeLists.txt similarity index 59% rename from database/CMakeLists.txt rename to server/database/CMakeLists.txt index 27bb557..948ccb8 100644 --- a/database/CMakeLists.txt +++ b/server/database/CMakeLists.txt @@ -5,7 +5,8 @@ project(Database LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 COMPONENTS Core Sql REQUIRED) +find_package(OpenSSL REQUIRED) +find_package(Qt6 COMPONENTS Widgets Core Sql REQUIRED) file(GLOB SOURCES "src/*.cpp") file(GLOB HEADERS "include/*.hpp") @@ -16,6 +17,7 @@ target_include_directories(Database PUBLIC $ $ + ${CMAKE_SOURCE_DIR}/client/ui/note-widget/include ) -target_link_libraries(Database PRIVATE Qt6::Core Qt6::Sql Qt6::Widgets Scripts) \ No newline at end of file +target_link_libraries(Database PRIVATE OpenSSL::Crypto Qt6::Widgets Qt6::Core Qt6::Sql NoteWidget model-proto pqxx ) diff --git a/server/database/include/database_manager.hpp b/server/database/include/database_manager.hpp new file mode 100644 index 0000000..7c7956c --- /dev/null +++ b/server/database/include/database_manager.hpp @@ -0,0 +1,19 @@ +#ifndef DATABASE_MANAGER_HPP +#define DATABASE_MANAGER_HPP + +#include +#include "note_dao.hpp" + +class DatabaseManager final { +public: + static DatabaseManager &get_instance(); + pqxx::connection &get_connection(); + [[nodiscard]] static std::string get_connection_string(); + +private: + explicit DatabaseManager(); + ~DatabaseManager() = default; + std::unique_ptr connection_; +}; + +#endif // DATABASE_MANAGER_HPP \ No newline at end of file diff --git a/server/database/include/lr_dao.hpp b/server/database/include/lr_dao.hpp new file mode 100644 index 0000000..81b7714 --- /dev/null +++ b/server/database/include/lr_dao.hpp @@ -0,0 +1,35 @@ +#ifndef LRDAO_H +#define LRDAO_H + +#include +#include + +class LRDao { +public: + LRDao() = default; + static bool validate_password( + const std::string &input_password, + const std::string &stored_hash + ); + static int + try_register_user(const std::string &login, const std::string &password); + static bool + validate_user(const std::string &login, const std::string &password); + static bool add_project_to_user( + const std::string &user_login, + const std::string &project_code + ); + static bool get_user_projects( + const std::string &login, + std::vector &projects + ); + static bool delete_project_from_user( + const std::string &login, + const std::string &project_code + ); + +private: + static std::string hash_password(const std::string &password); +}; + +#endif // LRDAO_H \ No newline at end of file diff --git a/server/database/include/note_dao.hpp b/server/database/include/note_dao.hpp new file mode 100644 index 0000000..6cd8cf4 --- /dev/null +++ b/server/database/include/note_dao.hpp @@ -0,0 +1,16 @@ +#ifndef NOTEDAO_HPP +#define NOTEDAO_HPP + +#include "model-proto/model.pb.h" + +using Efficio_proto::Note; + +class NoteDao { +public: + NoteDao() = default; + static Note initialize_note_for_user(const std::string &login); + static bool update_note(const Note ¬e); + [[nodiscard]] static Note get_note(int note_id); +}; + +#endif // NOTEDAO_HPP \ No newline at end of file diff --git a/server/database/include/project_dao.hpp b/server/database/include/project_dao.hpp new file mode 100644 index 0000000..b4b52e0 --- /dev/null +++ b/server/database/include/project_dao.hpp @@ -0,0 +1,74 @@ +#ifndef PROJECT_DAO_HPP +#define PROJECT_DAO_HPP + +#include +#include +#include +#include "database_manager.hpp" + +using namespace Efficio_proto; + +class ProjectDAO { + template + static bool change_project_array( + const std::string &project_code, + const T ¶meter, + const std::string &field, + const std::string &operation + ) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string query = + "UPDATE projects " + "SET " + + field + " = array_" + operation + "(" + field + + ", $1) " + "WHERE code = $2 " + "RETURNING 1;"; + + const pqxx::result result = + transaction.exec_params(query, parameter, project_code); + if (result.empty()) { + return false; + } + transaction.commit(); + return true; + } + +public: + ProjectDAO() = default; + static bool get_all_user_projects(const std::string& login, Storage &storage); + static bool get_project(const std::string &project_code, Project &project); + static bool insert_project(Project &project); + static bool add_member_to_project( + const std::string &project_code, + const std::string &member + ); + static bool delete_member_from_project( + const std::string &project_code, + const std::string &member + ); + static bool + add_note_to_project(const std::string &project_code, int note_id); + static bool + delete_note_from_project(const std::string &project_code, int note_id); + static bool change_project_title( + const std::string &project_code, + const std::string &new_title + ); + static bool code_available(const std::string &project_code); +}; + +template +std::vector proto_arr_to_vector( + const google::protobuf::RepeatedPtrField &array +) { + std::vector vec; + for (int i = 0; i < array.size(); ++i) { + vec.push_back(array[i]); + } + return std::move(vec); +} + +#endif diff --git a/server/database/src/database_manager.cpp b/server/database/src/database_manager.cpp new file mode 100644 index 0000000..5e0b5c3 --- /dev/null +++ b/server/database/src/database_manager.cpp @@ -0,0 +1,59 @@ +#include "database_manager.hpp" +#include + +DatabaseManager::DatabaseManager() { + connection_ = std::make_unique(get_connection_string()); + pqxx::work transaction(*connection_); + + transaction.exec( + "CREATE TABLE IF NOT EXISTS notes (" + "id SERIAL PRIMARY KEY, " + "title TEXT NOT NULL, " + "content TEXT, " + "user_id VARCHAR(50) REFERENCES users(login), " + "members VARCHAR(50)[], " + "date VARCHAR(50), " + "tags VARCHAR(50)[] " + ")" + ); + + transaction.exec( + "CREATE TABLE IF NOT EXISTS users (" + "login VARCHAR(50) PRIMARY KEY, " + "password VARCHAR(50) NOT NULL, " + "token VARCHAR(100), " + "projects VARCHAR(6)[]" + ")" + ); + + transaction.exec( + "CREATE TABLE IF NOT EXISTS projects (" + "code VARCHAR(6) PRIMARY KEY, " + "title VARCHAR(50) NOT NULL, " + "notes INT[], " + "members VARCHAR(50)[]" + ")" + ); + + transaction.commit(); +} + +DatabaseManager &DatabaseManager::get_instance() { + static DatabaseManager instance; + if (!instance.connection_->is_open()) { + throw std::runtime_error("Lost connection with database"); + } + return instance; +} + +pqxx::connection &DatabaseManager::get_connection() { + if (!connection_->is_open()) { + connection_ = + std::make_unique(get_connection_string()); + } + return *connection_; +} + +std::string DatabaseManager::get_connection_string() { + return "postgresql://efficio:admin@localhost/efficio"; +} \ No newline at end of file diff --git a/server/database/src/lr_dao.cpp b/server/database/src/lr_dao.cpp new file mode 100644 index 0000000..b903749 --- /dev/null +++ b/server/database/src/lr_dao.cpp @@ -0,0 +1,193 @@ +#include "lr_dao.hpp" +#include +#include +#include +#include +#include +#include +#include "database_manager.hpp" + +std::string LRDao::hash_password(const std::string &password) { + if (password.empty()) { + throw std::runtime_error("Password cannot be empty"); + } + + EVP_MD_CTX *mdctx = EVP_MD_CTX_new(); + if (!mdctx) { + throw std::runtime_error("Failed to create EVP_MD_CTX"); + } + + if (EVP_DigestInit_ex(mdctx, EVP_sha256(), nullptr) != 1) { + EVP_MD_CTX_free(mdctx); + throw std::runtime_error("Failed to initialize digest"); + } + + if (EVP_DigestUpdate(mdctx, password.c_str(), password.size()) != 1) { + EVP_MD_CTX_free(mdctx); + throw std::runtime_error("Failed to update digest"); + } + + std::array hash{}; + unsigned int length = 0; + if (EVP_DigestFinal_ex(mdctx, hash.data(), &length) != 1) { + EVP_MD_CTX_free(mdctx); + throw std::runtime_error("Failed to finalize digest"); + } + + EVP_MD_CTX_free(mdctx); + + std::stringstream ss; + for (const auto &byte : hash) { + ss << std::hex << std::setw(2) << std::setfill('0') + << static_cast(byte); + } + + std::string full_hash = ss.str(); + + if (full_hash.length() > 49) { + return full_hash.substr(0, 49); + } + + return full_hash; +} + +bool LRDao::validate_password( + const std::string &input_password, + const std::string &stored_hash +) { + const std::string input_hash = hash_password(input_password); + return input_hash == stored_hash; +} + +int LRDao::try_register_user( + const std::string &login, + const std::string &password +) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string check_query = "SELECT * FROM users WHERE login = $1"; + + const pqxx::result check_result = + transaction.exec_params(check_query, login); + + if (!check_result.empty()) { + transaction.commit(); + return -1; + } + transaction.commit(); + const std::string hashed_password = hash_password(password); + + pqxx::work transaction_insert(connection); + const std::string insert_query = + "INSERT INTO users (login, password) VALUES ($1, $2)"; + + const pqxx::result insert_result = + transaction_insert.exec_params(insert_query, login, hashed_password); + + transaction_insert.commit(); + return insert_result.affected_rows() > 0 ? 1 : 0; +} + +bool LRDao::validate_user( + const std::string &login, + const std::string &password +) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + std::cout << "[SERVER]: AUTHENTICATING USER - " << login << "\n"; + + const std::string query = "SELECT password FROM users WHERE login = $1"; + + const pqxx::result result = transaction.exec_params(query, login); + + if (result.empty()) { + transaction.commit(); + return false; + } + + const auto stored_hash = result[0]["password"].as(); + const bool is_valid = validate_password(password, stored_hash); + + transaction.commit(); + return is_valid; +} + +bool LRDao::add_project_to_user( + const std::string &user_login, + const std::string &project_code +) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string query = + "UPDATE users SET projects = array_append(projects, $1) " + "WHERE login = $2"; + // pqxx::params params; + // params.append(project_code); + // params.append(user_login); + + const pqxx::result result = + transaction.exec_params(query, project_code, user_login); + transaction.commit(); + return result.affected_rows() > 0; +} + +bool LRDao::get_user_projects( + const std::string &login, + std::vector &projects +) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string query = "SELECT projects FROM users WHERE login = $1"; + + const pqxx::result result = transaction.exec_params(query, login); + + if (!result.empty()) { + const auto &row = result[0]; + if (!row["projects"].is_null()) { + const auto projects_as_string = row["projects"].as(); + + std::string clean_string = projects_as_string; + if (!clean_string.empty() && clean_string.front() == '{' && + clean_string.back() == '}') { + clean_string = clean_string.substr(1, clean_string.size() - 2); + } + + std::istringstream iss(clean_string); + std::string project_id; + while (std::getline(iss, project_id, ',')) { + if (!project_id.empty()) { + projects.push_back(project_id); + } + } + + transaction.commit(); + return true; + } + } + + transaction.commit(); + return false; +} + +bool LRDao::delete_project_from_user( + const std::string &login, + const std::string &project_code +) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + const std::string query = + "UPDATE users SET projects = array_remove(projects, $1) WHERE " + "login = $2" + "RETURNING 1"; + + const pqxx::result result = + transaction.exec_params(query, project_code, login); + transaction.commit(); + if (result.empty()) { + return false; + } + return true; +} diff --git a/server/database/src/note_dao.cpp b/server/database/src/note_dao.cpp new file mode 100644 index 0000000..81f5545 --- /dev/null +++ b/server/database/src/note_dao.cpp @@ -0,0 +1,189 @@ +#include "note_dao.hpp" +#include +#include "database_manager.hpp" +#include "tags_dialog.h" + +namespace { + +std::string format_members_array( + const google::protobuf::RepeatedPtrField &members +) { + std::string result = "{"; + bool first = true; + for (const auto &member : members) { + if (!first) { + result += ","; + } + result += "\"" + member + "\""; + first = false; + } + return result + "}"; +} + +std::string format_tags_array( + const google::protobuf::RepeatedPtrField &tags +) { + std::string result = "{"; + bool first = true; + for (const auto &tag : tags) { + if (!first) { + result += ","; + } + std::string color_code = + TagsDialog::get_color_by_code(tag.color()).toStdString(); + result += "\"{" + tag.text() + ":" + color_code + "}\""; + first = false; + } + return result + "}"; +} + +} // namespace + +Note NoteDao::initialize_note_for_user(const std::string &login) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string query = + "INSERT INTO notes (title, content) " + "VALUES ($1, $2) " + "RETURNING id, title, content"; + + const pqxx::result result = + transaction.exec_params(query, "Пустая заметка", ""); + + if (result.empty()) { + transaction.commit(); + return {}; + } + + const pqxx::row row = result[0]; + Note note; + note.set_id(row["id"].as()); + note.set_title(row["title"].as()); + + transaction.commit(); + return note; +} + +bool NoteDao::update_note(const Note ¬e) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string members_array = format_members_array(note.members()); + const std::string tags_array = format_tags_array(note.tags()); + + const std::string query = + "UPDATE notes SET " + "title = $1, " + "content = $2, " + "members = $3::varchar(50)[], " + "date = $4, " + "tags = $5::varchar(50)[] " + "WHERE id = $6"; + + pqxx::params params; + params.append(note.title()); + params.append(note.text()); + params.append(members_array); + params.append(note.date()); + params.append(tags_array); + params.append(note.id()); + + const pqxx::result result = transaction.exec_params( + query, note.title(), note.text(), members_array, note.date(), + tags_array, note.id() + ); + transaction.commit(); + return result.affected_rows() > 0; +} + +Efficio_proto::Note_tag::colors color_hex_to_enum(const std::string &hex) { + static const std::unordered_map< + std::string, Efficio_proto::Note_tag::colors> + color_map = { + {"e7624b", Efficio_proto::Note_tag::Red}, + {"165d7b", Efficio_proto::Note_tag::Blue}, + {"bd6dab", Efficio_proto::Note_tag::Pink}, + {"00b16b", Efficio_proto::Note_tag::Green}, + {"e69f00", Efficio_proto::Note_tag::Yellow} + }; + auto it = color_map.find(hex); + return it != color_map.end() ? it->second : Efficio_proto::Note_tag::Red; +} + +Note NoteDao::get_note(const int note_id) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string query = "SELECT * FROM notes WHERE id = $1"; + + const pqxx::result result = transaction.exec_params(query, note_id); + + if (result.empty()) { + transaction.commit(); + return {}; + } + + const pqxx::row row = result[0]; + Note note; + + note.set_id(row["id"].as()); + note.set_title(row["title"].as()); + note.set_text(row["content"].as()); + + if (!row["date"].is_null()) { + note.set_date(row["date"].as()); + } + + if (!row["members"].is_null()) { + const auto members_as_string = row["members"].as(); + + if (members_as_string.size() > 2) { + std::stringstream ss( + members_as_string.substr(1, members_as_string.size() - 2) + ); + std::string member; + + while (std::getline(ss, member, ',')) { + std::erase(member, '"'); + if (!member.empty()) { + note.add_members(member); + } + } + } + } + + if (!row["tags"].is_null()) { + const auto tags_as_string = row["tags"].as(); + + if (tags_as_string.size() > 2) { + std::stringstream ss( + tags_as_string.substr(1, tags_as_string.size() - 2) + ); + std::string tag_item; + + while (std::getline(ss, tag_item, ',')) { + std::erase(tag_item, '"'); + + if (tag_item.size() > 3 && tag_item[0] == '{' && + tag_item.back() == '}') { + const auto colon_position = tag_item.find(':'); + + if (colon_position != std::string::npos) { + auto *tag = note.add_tags(); + tag->set_text(tag_item.substr(1, colon_position - 1)); + + const std::string color_hex = tag_item.substr( + colon_position + 2, + tag_item.size() - colon_position - 3 + ); + tag->set_color(color_hex_to_enum(color_hex)); + } + } + } + } + } + + transaction.commit(); + return note; +} \ No newline at end of file diff --git a/server/database/src/project_dao.cpp b/server/database/src/project_dao.cpp new file mode 100644 index 0000000..96858cd --- /dev/null +++ b/server/database/src/project_dao.cpp @@ -0,0 +1,208 @@ +#include "project_dao.hpp" +#include +#include "database_manager.hpp" +#include "note_dao.hpp" + +bool ProjectDAO::get_all_user_projects( + const std::string &login, + Storage &storage +) { + pqxx::connection &connection = + DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string projects_query = + "SELECT p.code, p.title, p.members " + "FROM projects p " + "JOIN users u ON p.code::text = ANY(u.projects::text[]) " + "WHERE u.login = " + + transaction.quote(login); + + const pqxx::result projects_result = transaction.exec(projects_query); + + if (projects_result.empty()) { + transaction.commit(); + return false; + } + + for (const auto &project_row : projects_result) { + Project project; + project.set_code(project_row["code"].as()); + project.set_title(project_row["title"].as()); + + // if (!project_row["members"].is_null()) { + // auto members_str = project_row["members"].as(); + // + // if (!members_str.empty() && members_str.front() == '{' && + // members_str.back() == '}') { + // members_str = members_str.substr(1, members_str.size() - 2); + // std::istringstream iss(members_str); + // std::string member; + // while (std::getline(iss, member, ',')) { + // if (member.size() >= 2 && member.front() == '"' && + // member.back() == '"') { + // member = member.substr(1, member.size() - 2); + // } + // + // std::erase_if(member, ::isspace); + // if (!member.empty()) { + // project.add_members(member); + // } + // } + // } + // } + + storage.mutable_projects()->Add(std::move(project)); + } + + transaction.commit(); + return true; +} + +bool ProjectDAO::get_project(const std::string &code, Project &project) { + pqxx::connection &connection = + DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string query = "SELECT * FROM projects WHERE code = $1"; + + const pqxx::result result = transaction.exec_params(query, code); + + if (result.empty()) { + transaction.commit(); + return false; + } + transaction.commit(); + const pqxx::row row = result[0]; + + project.set_title(row["title"].as()); + project.set_code(row["code"].as()); + + if (!row["members"].is_null()) { + const auto members_as_string = row["members"].as(); + + if (members_as_string.size() > 2) { + std::stringstream ss( + members_as_string.substr(1, members_as_string.size() - 2) + ); + std::string member; + + while (std::getline(ss, member, ',')) { + std::erase(member, '"'); + if (!member.empty()) { + project.add_members(member); + } + } + } + } + + if (!row["notes"].is_null()) { + const auto notes_as_string = row["notes"].as(); + + if (notes_as_string.size() > 2) { + std::stringstream ss( + notes_as_string.substr(1, notes_as_string.size() - 2) + ); + std::string note_id_string; + + while (std::getline(ss, note_id_string, ',')) { + std::erase(note_id_string, '"'); + if (!note_id_string.empty()) { + Note *note = project.add_notes(); + *note = NoteDao::get_note(std::stoi(note_id_string)); + } + } + } + } + + return true; +} + +bool ProjectDAO::insert_project(Project &project) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string query = + "INSERT INTO projects (code, title, notes, members) " + "VALUES ($1, $2, $3, $4) "; + + std::vector note_ids; + for (auto note : project.notes()) { + note_ids.push_back(note.id()); + } + + const pqxx::result result = transaction.exec_params( + query, project.code(), project.title(), note_ids, + proto_arr_to_vector(project.members()) + ); + + transaction.commit(); + return true; +} + +bool ProjectDAO::add_member_to_project( + const std::string &project_code, + const std::string &member +) { + return change_project_array(project_code, member, "members", "append"); +} + +bool ProjectDAO::delete_member_from_project( + const std::string &project_code, + const std::string &member +) { + return change_project_array(project_code, member, "members", "remove"); +} + +bool ProjectDAO::add_note_to_project( + const std::string &project_code, + int note_id +) { + return change_project_array(project_code, note_id, "notes", "append"); +} + +bool ProjectDAO::change_project_title( + const std::string &project_code, + const std::string &new_title +) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string query = + "UPDATE projects " + "SET title = $1 " + "WHERE code = $2 " + "RETURNING 1;"; + + const pqxx::result result = + transaction.exec_params(query, new_title, project_code); + if (result.empty()) { + return false; + } + + transaction.commit(); + return true; +} + +bool ProjectDAO::delete_note_from_project( + const std::string &project_code, + int note_id +) { + return change_project_array(project_code, note_id, "notes", "remove"); +} + +bool ProjectDAO::code_available(const std::string &project_code) { + auto &connection = DatabaseManager::get_instance().get_connection(); + pqxx::work transaction(connection); + + const std::string query = + "SELECT code " + "FROM projects " + "WHERE code = $1 "; + const pqxx::result result = transaction.exec_params(query, project_code); + transaction.commit(); + if (result.empty()) { + return true; + } + return false; +} diff --git a/server/main.cpp b/server/main.cpp new file mode 100644 index 0000000..b287d07 --- /dev/null +++ b/server/main.cpp @@ -0,0 +1,13 @@ +#include "server_implementation.h" +#include +#include + +int main() { + std::cout << "gRPC version : " << GRPC_CPP_VERSION_STRING << std::endl; + + grpc::ServerBuilder builder; + ServerImplementation server(50051, builder); + std::cout << "[SERVER]: WAITING FOR REQUESTS...\n"; + server.HandleRPCs(); + return 0; +} \ No newline at end of file diff --git a/ui/authorization-windows/include/login_window.h b/ui/authorization-windows/include/login_window.h deleted file mode 100644 index 5aed300..0000000 --- a/ui/authorization-windows/include/login_window.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include "database_manager.hpp" -#include "lr_dao.hpp" - -QT_BEGIN_NAMESPACE - -namespace Ui { -class LoginWindow; -} - -QT_END_NAMESPACE - -class LoginWindow : public QWidget { - Q_OBJECT - -public: - explicit LoginWindow(QWidget *parent = nullptr); - ~LoginWindow(); - -private slots: - void on_switch_mode_clicked(); - void on_push_enter_clicked(); - -private: - Ui::LoginWindow *ui; -}; \ No newline at end of file diff --git a/ui/authorization-windows/include/login_window_style_sheet.h b/ui/authorization-windows/include/login_window_style_sheet.h deleted file mode 100644 index 3a6d776..0000000 --- a/ui/authorization-windows/include/login_window_style_sheet.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include "ui_login_window.h" - -namespace Ui { -QString login_window_light_theme = R"( - QWidget { - background-color: #f5f5f5; - } - - QLabel { - font-family: 'Arial'; - font-weight: bold; - font-size: 13px; - color: #089083; - padding: 1px; - background-color: transparent; - } - - QPushButton#pushEnter { - font-family: 'Arial'; - font-weight: bold; - border-radius: 10px; - background-color: #fea36b; - color: white; - padding: 5px 10px; - } - - QPushButton#switchMode { - font-family: 'Arial'; - font-weight: bold; - border-radius: 10px; - background-color: white; - color: #fea36b; - padding: 5px 10px; - } - - QPushButton#pushEnter:hover { - background-color: #d58745; - } - - QPushButton#switchMode:hover { - background-color: #dadada; - } - - QLineEdit { - border-radius: 10px; - border: 1px solid white; - background: white; - color: black; - padding: 5px; - } - - QLineEdit::placeholder { - color: #727272; - } - -)"; - -} // namespace Ui \ No newline at end of file diff --git a/ui/authorization-windows/include/registration_window.h b/ui/authorization-windows/include/registration_window.h deleted file mode 100644 index 35f4bc4..0000000 --- a/ui/authorization-windows/include/registration_window.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include -#include -#include "database_manager.hpp" -#include "lr_dao.hpp" - -QT_BEGIN_NAMESPACE - -namespace Ui { -class RegistrationWindow; -} - -QT_END_NAMESPACE - -class RegistrationWindow : public QWidget { - Q_OBJECT - -public: - explicit RegistrationWindow(QWidget *parent = nullptr); - ~RegistrationWindow(); - bool is_strong_and_valid_password(const QString &password); - -private slots: - void on_switch_mode_clicked(); - void on_push_registration_clicked(); - -private: - Ui::RegistrationWindow *ui; -}; \ No newline at end of file diff --git a/ui/authorization-windows/include/registration_window_style_sheet.h b/ui/authorization-windows/include/registration_window_style_sheet.h deleted file mode 100644 index dc7a1ca..0000000 --- a/ui/authorization-windows/include/registration_window_style_sheet.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include "ui_registration_window.h" - -namespace Ui { -QString registration_window_light_theme = R"( - QWidget { - background-color: #f5f5f5; - } - - QLabel { - font-family: 'Arial'; - background-color: transparent; - font-size: 13px; - color: #089083; - padding: 1px; - } - - QPushButton#pushRegistration { - font-weight: bold; - font-family: 'Arial'; - border-radius: 8px; - background-color: #fea36b; - color: white; - padding: 5px 10px; - } - - QPushButton#switchMode { - font-family: 'Arial'; - border-radius: 10px; - background-color: white; - color: #fea36b; - padding: 5px 10px; - } - - QPushButton#pushRegistration:hover { - background-color: #d58745; - } - - QPushButton#switchMode:hover { - background-color: #dadada; - } - - QLineEdit { - border-radius: 10px; - border: 1px solid white; - background: white; - color: black; - padding: 5px; - } - - QLineEdit::placeholder { - color: #727272; - } - -)"; - -} // namespace Ui \ No newline at end of file diff --git a/ui/authorization-windows/src/login_window.cpp b/ui/authorization-windows/src/login_window.cpp deleted file mode 100644 index 0583ef5..0000000 --- a/ui/authorization-windows/src/login_window.cpp +++ /dev/null @@ -1,118 +0,0 @@ -#include "login_window.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include "applicationwindow.h" -#include "bottombar.h" -#include "database_manager.hpp" -#include "login_window_style_sheet.h" -#include "lr_dao.hpp" -#include "mainwindow.h" -#include "notelist.h" -#include "registration_window.h" -#include "serialization.hpp" - -LoginWindow::LoginWindow(QWidget *parent) - : QWidget(parent), ui(new Ui::LoginWindow) { - ui->setupUi(this); - - setFixedSize(380, 480); - ui->inputLogin->setPlaceholderText("Введите логин:"); - ui->inputPassword->setPlaceholderText("Введите пароль:"); - setStyleSheet(Ui::login_window_light_theme); - ui->inputPassword->setEchoMode(QLineEdit::Password); - - connect( - ui->switchMode, &QPushButton::clicked, this, - &LoginWindow::on_switch_mode_clicked - ); - connect( - ui->pushEnter, &QPushButton::clicked, this, - &LoginWindow::on_push_enter_clicked - ); -} - -LoginWindow::~LoginWindow() { - delete ui; -} - -void LoginWindow::on_switch_mode_clicked() { - QWidget *parent = this->parentWidget(); - - QMainWindow *app_window = qobject_cast(parent); - - if (QWidget *old = app_window->centralWidget()) { - old->deleteLater(); - } - project_storage_model::Storage storage; - RegistrationWindow *registration_window = - new RegistrationWindow(app_window); - - app_window->setCentralWidget(registration_window); - QRect screenGeometry = QApplication::primaryScreen()->availableGeometry(); - int x = (screenGeometry.width() - registration_window->width()) / 2; - int y = (screenGeometry.height() - registration_window->height()) / 2; - app_window->move(x, y); - - this->close(); -} - -void LoginWindow::on_push_enter_clicked() { - QString login = ui->inputLogin->text(); - QString password = ui->inputPassword->text(); - - if (!login.isEmpty() && !password.isEmpty()) { - if (login.size() > 50) { - QMessageBox::warning( - this, "Ошибка", - "Длина логина не должна превышать пятидесяти символов" - ); - } else if (password.size() > 50) { - QMessageBox::warning( - this, "Ошибка", - "Длина пароля не должна превышать пятидесяти символов" - ); - } else if (LRDao::validate_user(login, password)) { - QMessageBox::information( - this, "Вход", "Вы успешно вошли! Добро пожаловать :)" - ); - QWidget *parent = this->parentWidget(); - - QMainWindow *app_window = qobject_cast(parent); - - if (QWidget *old = app_window->centralWidget()) { - old->deleteLater(); - } - // todo load all projects of user to storage - project_storage_model::Storage *storage = - new project_storage_model::Storage(); - Serialization::get_storage(*storage, login.toStdString()); - - Ui::MainWindow *main_window = - new Ui::MainWindow(app_window, login.toStdString(), storage); - - app_window->setCentralWidget(main_window); - app_window->resize(800, 600); - QRect screenGeometry = - QApplication::primaryScreen()->availableGeometry(); - int x = (screenGeometry.width() - main_window->width()) / 2; - int y = (screenGeometry.height() - main_window->height()) / 2; - app_window->move(x, y); - - this->close(); - } else { - QMessageBox::warning( - this, "Ошибка ввода данных", "Неверный логин или пароль!" - ); - } - } else { - QMessageBox::warning( - this, "Ошибка ввода данных", "Пожалуйста, заполните все поля!" - ); - } -} \ No newline at end of file diff --git a/ui/authorization-windows/src/registration_window.cpp b/ui/authorization-windows/src/registration_window.cpp deleted file mode 100644 index f30f7d8..0000000 --- a/ui/authorization-windows/src/registration_window.cpp +++ /dev/null @@ -1,154 +0,0 @@ -#include "registration_window.h" -#include -#include -#include -#include -#include -#include -#include -#include "applicationwindow.h" -#include "bottombar.h" -#include "database_manager.hpp" -#include "login_window.h" -#include "lr_dao.hpp" -#include "mainwindow.h" -#include "notelist.h" -#include "registration_window.h" -#include "registration_window_style_sheet.h" - -RegistrationWindow::RegistrationWindow(QWidget *parent) - : QWidget(parent), ui(new Ui::RegistrationWindow) { - ui->setupUi(this); - - setFixedSize(380, 480); - - ui->createLogin->setPlaceholderText("Введите логин:"); - ui->createPassword->setPlaceholderText("Введите пароль:"); - ui->repeatPassword->setPlaceholderText("Повторите пароль:"); - setStyleSheet(Ui::registration_window_light_theme); - ui->createPassword->setEchoMode(QLineEdit::Password); - ui->repeatPassword->setEchoMode(QLineEdit::Password); - - connect( - ui->pushRegistration, &QPushButton::clicked, this, - &RegistrationWindow::on_push_registration_clicked - ); - connect( - ui->switchMode, &QPushButton::clicked, this, - &RegistrationWindow::on_switch_mode_clicked - ); -} - -RegistrationWindow::~RegistrationWindow() { - delete ui; -} - -void RegistrationWindow::on_switch_mode_clicked() { - QWidget *parent = this->parentWidget(); - - QMainWindow *app_window = qobject_cast(parent); - - if (QWidget *old = app_window->centralWidget()) { - old->deleteLater(); - } - project_storage_model::Storage storage; - LoginWindow *login_window = new LoginWindow(app_window); - - app_window->setCentralWidget(login_window); - QRect screenGeometry = QApplication::primaryScreen()->availableGeometry(); - int x = (screenGeometry.width() - login_window->width()) / 2; - int y = (screenGeometry.height() - login_window->height()) / 2; - app_window->move(x, y); - - this->close(); -} - -bool RegistrationWindow::is_strong_and_valid_password(const QString &password) { - if (password.length() < 8) { - QMessageBox::warning( - nullptr, "Ошибка: недостаточно надежный пароль", - "Пароль должен содержать не менее восьми символов" - ); - return false; - } - - bool has_digit = false; - bool has_latin_letter = false; - - for (const QChar ch : password) { - if (!ch.isLetter() && !ch.isDigit()) { - QMessageBox::warning( - nullptr, "Ошибка", - "Пароль должен содержать только символы латиницы и цифры" - ); - return false; - } else if (ch.isLetter()) { - has_latin_letter = true; - } else if (ch.isDigit()) { - has_digit = true; - } - } - - if (!has_latin_letter) { - QMessageBox::warning( - nullptr, "Ошибка: недостаточно надежный пароль", - "Пароль должен содержать хотя бы одну букву" - ); - return false; - } - if (!has_digit) { - QMessageBox::warning( - nullptr, "Ошибка: недостаточно надежный пароль", - "Пароль должен содержать хотя бы одну цифру" - ); - return false; - } - - return true; -} - -void RegistrationWindow::on_push_registration_clicked() { - QString created_login = ui->createLogin->text(); - QString created_password = ui->createPassword->text(); - QString repeated_password = ui->repeatPassword->text(); - - if (!created_login.isEmpty() && !created_password.isEmpty() && - !repeated_password.isEmpty()) { - if (created_password != repeated_password) { - QMessageBox::warning(this, "Ошибка", "Пароли не совпадают!"); - } else if (created_login.size() > 50) { - QMessageBox::warning( - this, "Ошибка", - "Длина логина не должна превышать пятидесяти символов" - ); - } else if (created_password.size() > 50) { - QMessageBox::warning( - this, "Ошибка", - "Длина пароля не должна превышать пятидесяти символов" - ); - } else if (is_strong_and_valid_password(created_password)) { - int try_register_user = - LRDao::try_register_user(created_login, created_password); - if (try_register_user == 0) { - QMessageBox::warning( - this, "Ошибка", - "Извините, разрабы дауны и не подключили толком бд." - ); - } else if (try_register_user == -1) { - QMessageBox::warning( - this, "Ошибка", - "Пользователь с таким именем уже существует. Пожалуйста, " - "придумайте другое!" - ); - } else { - QMessageBox::information( - this, "Регистрация", - "Вы успешно зарегистрировались! Пожалуйста, выполните вход." - ); - on_switch_mode_clicked(); // TODO: fix - } - } - } else { - QMessageBox::warning(this, "Ошибка", "Пожалуйста, заполните все поля."); - } -} \ No newline at end of file diff --git a/ui/main-window/include/notewidget.h b/ui/main-window/include/notewidget.h deleted file mode 100644 index f1f8f6a..0000000 --- a/ui/main-window/include/notewidget.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef NOTEWIDGET_H -#define NOTEWIDGET_H - -#include -#include -#include -#include -#include -#include "note.hpp" - -namespace Ui { -class NoteWidget : public QWidget { - Q_OBJECT - const project_storage_model::Note *model_note_; - QVBoxLayout *main_layout_; - QPushButton *open_button_; - QLabel *title_label_; - QLabel *text_label_; - -public: - explicit NoteWidget( - QWidget *parent = nullptr, - const project_storage_model::Note *model_note = nullptr - ); - -private slots: - - void open_note_window() const; -}; -} // namespace Ui -#endif // NOTEWIDGET_H \ No newline at end of file diff --git a/ui/main-window/src/notewidget.cpp b/ui/main-window/src/notewidget.cpp deleted file mode 100644 index 3ced750..0000000 --- a/ui/main-window/src/notewidget.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "notewidget.h" -#include -#include -#include -#include "note.hpp" -#include "note_edit_dialog.h" - -namespace Ui { -NoteWidget::NoteWidget( - QWidget *parent, - const project_storage_model::Note *model_note -) - : QWidget(parent), - model_note_(model_note), - main_layout_(new QVBoxLayout(this)), - open_button_(new QPushButton("Открыть")) { - this->setObjectName("NoteWidget"); - this->setMinimumWidth(100); - this->setFixedHeight(100); - title_label_ = new QLabel(model_note_->get_title().c_str(), this); - text_label_ = new QLabel(model_note_->get_text().c_str(), this); - - title_label_->setStyleSheet("color: rgb(33, 44, 50);"); - text_label_->setStyleSheet("color: rgb(33, 44, 50);"); - - main_layout_->addWidget(title_label_); - main_layout_->addWidget(text_label_); - - text_label_->setWordWrap(false); - title_label_->setWordWrap(false); - text_label_->setTextInteractionFlags(Qt::TextSelectableByMouse); - - main_layout_->addWidget(open_button_); - - connect( - open_button_, &QPushButton::clicked, this, &NoteWidget::open_note_window - ); - this->setLayout(main_layout_); - this->setAttribute(Qt::WA_StyledBackground); -} - -void NoteWidget::open_note_window() const { - auto dialog = new ::NoteEditDialog( - const_cast(qobject_cast(this)), - const_cast(model_note_) - ); - dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->exec(); - text_label_->setText(model_note_->get_text().c_str()); - title_label_->setText(model_note_->get_title().c_str()); - main_layout_->update(); -} -} // namespace Ui \ No newline at end of file diff --git a/ui/main-window/src/projectitem.cpp b/ui/main-window/src/projectitem.cpp deleted file mode 100644 index 4f38e85..0000000 --- a/ui/main-window/src/projectitem.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "projectitem.h" -#include - -namespace Ui { -ProjectItem::ProjectItem( - QListWidget *list_view, - project_storage_model::Project *project -) - : project_(project), - QListWidgetItem(project->get_name().c_str(), list_view) { -} -} // namespace Ui \ No newline at end of file