Merge branch 'frontend-redesign' into 'main'
Rewrote the frontend Closes #13, #2, #16, #21, #7, #18, #19, #8, #9, and #20 See merge request root/fileserver!8
This commit is contained in:
		@@ -7,8 +7,9 @@ stages:
 | 
				
			|||||||
build_backend:
 | 
					build_backend:
 | 
				
			||||||
    stage: build
 | 
					    stage: build
 | 
				
			||||||
    cache:
 | 
					    cache:
 | 
				
			||||||
 | 
					        key: backend
 | 
				
			||||||
        paths:
 | 
					        paths:
 | 
				
			||||||
            - /root/.cache/vcpkg
 | 
					            - vcpkg_cache
 | 
				
			||||||
    script:
 | 
					    script:
 | 
				
			||||||
        - apt-get update
 | 
					        - apt-get update
 | 
				
			||||||
        - apt-get install g++ gcc make cmake git curl zip unzip tar python3 pkg-config -y
 | 
					        - apt-get install g++ gcc make cmake git curl zip unzip tar python3 pkg-config -y
 | 
				
			||||||
@@ -18,18 +19,19 @@ build_backend:
 | 
				
			|||||||
        - git clone https://github.com/Microsoft/vcpkg.git .
 | 
					        - git clone https://github.com/Microsoft/vcpkg.git .
 | 
				
			||||||
        - ./bootstrap-vcpkg.sh -disableMetrics
 | 
					        - ./bootstrap-vcpkg.sh -disableMetrics
 | 
				
			||||||
        - cd $SRC
 | 
					        - cd $SRC
 | 
				
			||||||
        - cmake -B build -S backend -DCMAKE_TOOLCHAIN_FILE=$TMP/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release
 | 
					        - mkdir -p vcpkg_cache
 | 
				
			||||||
 | 
					        - VCPKG_DEFAULT_BINARY_CACHE=$SRC/vcpkg_cache cmake -B build -S backend -DCMAKE_TOOLCHAIN_FILE=$TMP/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release
 | 
				
			||||||
        - cmake --build build
 | 
					        - cmake --build build
 | 
				
			||||||
        - cp build/backend server
 | 
					        - cp build/backend server
 | 
				
			||||||
    artifacts:
 | 
					    artifacts:
 | 
				
			||||||
        paths:
 | 
					        paths:
 | 
				
			||||||
            - server
 | 
					            - server
 | 
				
			||||||
        expire_in: 1h
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
test_and_build_frontend:
 | 
					test_and_build_frontend:
 | 
				
			||||||
    image: node:latest
 | 
					    image: node:latest
 | 
				
			||||||
    stage: build
 | 
					    stage: build
 | 
				
			||||||
    cache:
 | 
					    cache:
 | 
				
			||||||
 | 
					        key: frontend
 | 
				
			||||||
        paths:
 | 
					        paths:
 | 
				
			||||||
            - frontend/.yarn
 | 
					            - frontend/.yarn
 | 
				
			||||||
            - frontend/node_modules
 | 
					            - frontend/node_modules
 | 
				
			||||||
@@ -41,7 +43,6 @@ test_and_build_frontend:
 | 
				
			|||||||
    artifacts:
 | 
					    artifacts:
 | 
				
			||||||
        paths:
 | 
					        paths:
 | 
				
			||||||
            - frontend/dist/
 | 
					            - frontend/dist/
 | 
				
			||||||
        expire_in: 1h
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
package_server:
 | 
					package_server:
 | 
				
			||||||
    stage: package
 | 
					    stage: package
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										9
									
								
								backend/.idea/cmake.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								backend/.idea/cmake.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project version="4">
 | 
				
			||||||
 | 
					  <component name="CMakeSharedSettings">
 | 
				
			||||||
 | 
					    <configurations>
 | 
				
			||||||
 | 
					      <configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G Ninja --toolchain C:\vcpkg\scripts\buildsystems\vcpkg.cmake -DCMAKE_INSTALL_PREFIX=./cmake_install" />
 | 
				
			||||||
 | 
					      <configuration PROFILE_NAME="Release" ENABLED="true" CONFIG_NAME="Release" GENERATION_OPTIONS="-G Ninja --toolchain C:\vcpkg\scripts\buildsystems\vcpkg.cmake -DCMAKE_INSTALL_PREFIX=./cmake_install" />
 | 
				
			||||||
 | 
					    </configurations>
 | 
				
			||||||
 | 
					  </component>
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
							
								
								
									
										9
									
								
								backend/.idea/dataSources.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								backend/.idea/dataSources.xml
									
									
									
										generated
									
									
									
								
							@@ -1,18 +1,11 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
<project version="4">
 | 
					<project version="4">
 | 
				
			||||||
  <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
 | 
					  <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
 | 
				
			||||||
    <data-source source="LOCAL" name="sqlite.db" uuid="6e8086dd-b853-422e-b48a-7c96a2403352">
 | 
					 | 
				
			||||||
      <driver-ref>sqlite.xerial</driver-ref>
 | 
					 | 
				
			||||||
      <synchronize>true</synchronize>
 | 
					 | 
				
			||||||
      <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
 | 
					 | 
				
			||||||
      <jdbc-url>jdbc:sqlite:$PROJECT_DIR$/old_backend/sqlite.db</jdbc-url>
 | 
					 | 
				
			||||||
      <working-dir>$ProjectFileDir$</working-dir>
 | 
					 | 
				
			||||||
    </data-source>
 | 
					 | 
				
			||||||
    <data-source source="LOCAL" name="sqlite.db [2]" uuid="788293bd-abec-4b6b-a13e-26da21cb36dd">
 | 
					    <data-source source="LOCAL" name="sqlite.db [2]" uuid="788293bd-abec-4b6b-a13e-26da21cb36dd">
 | 
				
			||||||
      <driver-ref>sqlite.xerial</driver-ref>
 | 
					      <driver-ref>sqlite.xerial</driver-ref>
 | 
				
			||||||
      <synchronize>true</synchronize>
 | 
					      <synchronize>true</synchronize>
 | 
				
			||||||
      <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
 | 
					      <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
 | 
				
			||||||
      <jdbc-url>jdbc:sqlite:$PROJECT_DIR$/run/sqlite.db</jdbc-url>
 | 
					      <jdbc-url>jdbc:sqlite:$PROJECT_DIR$/../run/sqlite.db</jdbc-url>
 | 
				
			||||||
      <working-dir>$ProjectFileDir$</working-dir>
 | 
					      <working-dir>$ProjectFileDir$</working-dir>
 | 
				
			||||||
    </data-source>
 | 
					    </data-source>
 | 
				
			||||||
  </component>
 | 
					  </component>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,10 @@
 | 
				
			|||||||
cmake_minimum_required(VERSION 3.20)
 | 
					cmake_minimum_required(VERSION 3.21)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (WIN32 AND (NOT VCPKG_TARGET_TRIPLET))
 | 
				
			||||||
 | 
					    set(VCPKG_TARGET_TRIPLET x64-windows-static)
 | 
				
			||||||
 | 
					endif (WIN32 AND (NOT VCPKG_TARGET_TRIPLET))
 | 
				
			||||||
 | 
					#set(VCPKG_LIBRARY_LINKAGE static)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
project(backend)
 | 
					project(backend)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
set(CMAKE_CXX_STANDARD 20)
 | 
					set(CMAKE_CXX_STANDARD 20)
 | 
				
			||||||
@@ -13,13 +19,6 @@ add_executable(backend
 | 
				
			|||||||
        src/db/db.h
 | 
					        src/db/db.h
 | 
				
			||||||
        src/db/db.cpp
 | 
					        src/db/db.cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        src/db/model/Inode.cc
 | 
					 | 
				
			||||||
        src/db/model/Inode.h
 | 
					 | 
				
			||||||
        src/db/model/Tokens.cc
 | 
					 | 
				
			||||||
        src/db/model/Tokens.h
 | 
					 | 
				
			||||||
        src/db/model/User.cc
 | 
					 | 
				
			||||||
        src/db/model/User.h
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        src/controllers/controllers.h
 | 
					        src/controllers/controllers.h
 | 
				
			||||||
        src/controllers/admin.cpp
 | 
					        src/controllers/admin.cpp
 | 
				
			||||||
        src/controllers/fs.cpp
 | 
					        src/controllers/fs.cpp
 | 
				
			||||||
@@ -32,14 +31,23 @@ add_executable(backend
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        src/filters/filters.h
 | 
					        src/filters/filters.h
 | 
				
			||||||
        src/filters/filters.cpp
 | 
					        src/filters/filters.cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        model/Inode.cc
 | 
				
			||||||
 | 
					        model/Inode.h
 | 
				
			||||||
 | 
					        model/Tokens.cc
 | 
				
			||||||
 | 
					        model/Tokens.h
 | 
				
			||||||
 | 
					        model/User.cc
 | 
				
			||||||
 | 
					        model/User.h
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        SMTPMail-drogon-master/SMTPMail.cc
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
find_package(Drogon CONFIG REQUIRED)
 | 
					find_package(Drogon CONFIG REQUIRED)
 | 
				
			||||||
find_package(CURL CONFIG REQUIRED)
 | 
					 | 
				
			||||||
find_package(lodepng CONFIG REQUIRED)
 | 
					 | 
				
			||||||
find_package(OpenSSL REQUIRED)
 | 
					find_package(OpenSSL REQUIRED)
 | 
				
			||||||
 | 
					find_package(OpenCV CONFIG REQUIRED)
 | 
				
			||||||
 | 
					find_package(kubazip CONFIG REQUIRED)
 | 
				
			||||||
find_path(JWT_CPP_INCLUDE_DIRS "jwt-cpp/base.h")
 | 
					find_path(JWT_CPP_INCLUDE_DIRS "jwt-cpp/base.h")
 | 
				
			||||||
find_path(BOTAN_INCLUDE_DIRS "botan/botan.h")
 | 
					find_path(BOTAN_INCLUDE_DIRS "botan/botan.h")
 | 
				
			||||||
find_path(QR_INCLUDE_DIRS "qrcodegen.hpp")
 | 
					find_path(QR_INCLUDE_DIRS "qrcodegen.hpp")
 | 
				
			||||||
@@ -48,6 +56,10 @@ find_library(QR_LIBRARY nayuki-qr-code-generator)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
target_include_directories(backend PRIVATE
 | 
					target_include_directories(backend PRIVATE
 | 
				
			||||||
        src
 | 
					        src
 | 
				
			||||||
 | 
					        model
 | 
				
			||||||
 | 
					        shl
 | 
				
			||||||
 | 
					        SMTPMail-drogon-master
 | 
				
			||||||
 | 
					        ${OpenCV_INCLUDE_DIRS}
 | 
				
			||||||
        ${JWT_CPP_INCLUDE_DIRS}
 | 
					        ${JWT_CPP_INCLUDE_DIRS}
 | 
				
			||||||
        ${BOTAN_INCLUDE_DIRS}
 | 
					        ${BOTAN_INCLUDE_DIRS}
 | 
				
			||||||
        ${QR_INCLUDE_DIRS}
 | 
					        ${QR_INCLUDE_DIRS}
 | 
				
			||||||
@@ -55,14 +67,17 @@ target_include_directories(backend PRIVATE
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
target_link_libraries(backend
 | 
					target_link_libraries(backend
 | 
				
			||||||
        Drogon::Drogon
 | 
					        Drogon::Drogon
 | 
				
			||||||
        CURL::libcurl
 | 
					 | 
				
			||||||
        lodepng
 | 
					 | 
				
			||||||
        OpenSSL::SSL
 | 
					        OpenSSL::SSL
 | 
				
			||||||
 | 
					        kubazip::kubazip
 | 
				
			||||||
 | 
					        ${OpenCV_LIBS}
 | 
				
			||||||
        ${BOTAN_LIBRARY}
 | 
					        ${BOTAN_LIBRARY}
 | 
				
			||||||
        ${QR_LIBRARY}
 | 
					        ${QR_LIBRARY}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
install(TARGETS backend)
 | 
					set_property(TARGET backend PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					install(TARGETS backend RUNTIME_DEPENDENCY_SET backend_deps DESTINATION .)
 | 
				
			||||||
 | 
					install(RUNTIME_DEPENDENCY_SET backend_deps)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if(NOT MSVC)
 | 
					if(NOT MSVC)
 | 
				
			||||||
    target_compile_options(backend PRIVATE
 | 
					    target_compile_options(backend PRIVATE
 | 
				
			||||||
@@ -74,5 +89,6 @@ else()
 | 
				
			|||||||
endif(NOT MSVC)
 | 
					endif(NOT MSVC)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if(WIN32)
 | 
					if(WIN32)
 | 
				
			||||||
    target_compile_definitions(backend PRIVATE NOMINMAX)
 | 
					    target_link_libraries(backend iphlpapi)
 | 
				
			||||||
 | 
					    target_compile_definitions(backend PRIVATE NOMINMAX _WIN32_WINNT=0x0A00)
 | 
				
			||||||
endif()
 | 
					endif()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										21
									
								
								backend/SMTPMail-drogon-master/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								backend/SMTPMail-drogon-master/LICENSE
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					MIT License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright (c) 2020 ihmc3jn09hk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
				
			||||||
 | 
					of this software and associated documentation files (the "Software"), to deal
 | 
				
			||||||
 | 
					in the Software without restriction, including without limitation the rights
 | 
				
			||||||
 | 
					to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
				
			||||||
 | 
					copies of the Software, and to permit persons to whom the Software is
 | 
				
			||||||
 | 
					furnished to do so, subject to the following conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all
 | 
				
			||||||
 | 
					copies or substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
				
			||||||
 | 
					IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
				
			||||||
 | 
					FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
				
			||||||
 | 
					AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
				
			||||||
 | 
					LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
				
			||||||
 | 
					OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
				
			||||||
 | 
					SOFTWARE.
 | 
				
			||||||
							
								
								
									
										79
									
								
								backend/SMTPMail-drogon-master/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								backend/SMTPMail-drogon-master/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					# SMTPMail-drogon
 | 
				
			||||||
 | 
					Simple Mail for the Drogon framework.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It is made as a plugin for the [drogon](https://github.com/an-tao/drogon) framework.
 | 
				
			||||||
 | 
					It can be included into the drogon build with little
 | 
				
			||||||
 | 
					modification of the class declaration.
 | 
				
			||||||
 | 
					## Updates 
 | 
				
			||||||
 | 
					- **[ 13-06-2022 ] Fixed vulnerability issues reported by [Sam](https://snoopysecurity.github.io/about).**
 | 
				
			||||||
 | 
					- [ 13-09-2021 ] Added [HTML content support](https://github.com/ihmc3jn09hk/SMTPMail-drogon/pull/1).
 | 
				
			||||||
 | 
					- [ 23-12-2020 ] Added DNS support.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Acknowledgement
 | 
				
			||||||
 | 
					* The implementation takes SMTPClient for Qt from [kelvins](https://github.com/kelvins/SMTPClient) as reference.
 | 
				
			||||||
 | 
					* There requires a delay SSL encryption from the Tcp-socket (named TcpClient in trantor/drogon) and the major
 | 
				
			||||||
 | 
					author of drogon [reponsed](https://github.com/an-tao/drogon/issues/346) quickly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Usage
 | 
				
			||||||
 | 
					Download to the plugin directory of the target drogon app, E.g. ~/drogon-app/plugins
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					$ git clone https://github.com/ihmc3jn09hk/SMTPMail-drogon.git
 | 
				
			||||||
 | 
					$ cp SMTPMail-drogon/SMTPMail.* ~/drogon-app/plugins
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* _Be aware of add the plugin into the config.json. Set the "name" field to "SMTPMail"_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Add the reference header and get the plugin from the app(), E.g.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```c++
 | 
				
			||||||
 | 
					...
 | 
				
			||||||
 | 
					#include "../plugins/SMTPMail.h"
 | 
				
			||||||
 | 
					...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//Inside some function, E.g. A controller function.
 | 
				
			||||||
 | 
					...
 | 
				
			||||||
 | 
					//Send an email
 | 
				
			||||||
 | 
					auto *smtpmailPtr = app().getPlugin<SMTPMail>();
 | 
				
			||||||
 | 
					auto id = smtpmailPtr->sendEmail(
 | 
				
			||||||
 | 
					          "127.0.0.1",                  //The server IP/DNS
 | 
				
			||||||
 | 
					          587,                          //The port
 | 
				
			||||||
 | 
					          "mailer@something.com",       //Who send the email
 | 
				
			||||||
 | 
					          "receiver@otherthing.com",    //Send to whom
 | 
				
			||||||
 | 
					          "Testing SMTPMail Function",  //Email Subject/Title
 | 
				
			||||||
 | 
					          "Hello from drogon plugin",   //Content
 | 
				
			||||||
 | 
					          "mailer@something.com",       //Login user
 | 
				
			||||||
 | 
					          "123456",                     //User password
 | 
				
			||||||
 | 
					          false                         //Is HTML content
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					...
 | 
				
			||||||
 | 
					//Or get noted when email is sent
 | 
				
			||||||
 | 
					...
 | 
				
			||||||
 | 
					void callback(const std::string &msg)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  LOG_INFO << msg; /*Output e.g. "EMail sent. ID : 96ESERVDDFH17588ECF0C7B00326E3"*/
 | 
				
			||||||
 | 
					  /*Do whatever you like*/
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					...
 | 
				
			||||||
 | 
					auto *smtpmailPtr = app().getPlugin<SMTPMail>();
 | 
				
			||||||
 | 
					auto id = smtpmailPtr->sendEmail(
 | 
				
			||||||
 | 
					          "127.0.0.1",                  //The server IP/DNS
 | 
				
			||||||
 | 
					          587,                          //The port
 | 
				
			||||||
 | 
					          "mailer@something.com",       //Who send the email
 | 
				
			||||||
 | 
					          "receiver@otherthing.com",    //Send to whom
 | 
				
			||||||
 | 
					          "Testing SMTPMail Function",  //Email Subject/Title
 | 
				
			||||||
 | 
					          "Hello from drogon plugin",   //Content
 | 
				
			||||||
 | 
					          "mailer@something.com",       //Login user
 | 
				
			||||||
 | 
					          "123456",                     //User password
 | 
				
			||||||
 | 
					          false,                        //Is HTML content
 | 
				
			||||||
 | 
					          callback                      //Callback
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					$ cd ~/drogon-app/build
 | 
				
			||||||
 | 
					$ make
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Licence
 | 
				
			||||||
 | 
					* Feel free to use, thanks to open-source.
 | 
				
			||||||
 | 
					* For the sake of concern on commercial usage, a simple licence is included in each of the files.
 | 
				
			||||||
							
								
								
									
										400
									
								
								backend/SMTPMail-drogon-master/SMTPMail.cc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										400
									
								
								backend/SMTPMail-drogon-master/SMTPMail.cc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,400 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					*
 | 
				
			||||||
 | 
					* SMTPMail.cc *
 | 
				
			||||||
 | 
					*
 | 
				
			||||||
 | 
					* This plugin is for SMTP mail delivery for the Drogon web-framework.
 | 
				
			||||||
 | 
					Implementation
 | 
				
			||||||
 | 
					* reference from the project "SMTPClient" with Qt5 by kelvins. Please check out
 | 
				
			||||||
 | 
					* https://github.com/kelvins/SMTPClient.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Feel free to use the code. For the sake of any concern, the following licence is
 | 
				
			||||||
 | 
					attached.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Copyright 2020 ihmc3jn09hk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy of
 | 
				
			||||||
 | 
					this software and associated documentation files (the "Software"), to deal in
 | 
				
			||||||
 | 
					the Software without restriction, including without limitation the rights to
 | 
				
			||||||
 | 
					use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 | 
				
			||||||
 | 
					the Software, and to permit persons to whom the Software is furnished to do so,
 | 
				
			||||||
 | 
					subject to the following conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all
 | 
				
			||||||
 | 
					copies or substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
				
			||||||
 | 
					IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 | 
				
			||||||
 | 
					FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 | 
				
			||||||
 | 
					COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 | 
				
			||||||
 | 
					IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 | 
				
			||||||
 | 
					CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "SMTPMail.h"
 | 
				
			||||||
 | 
					#include <drogon/HttpAppFramework.h>
 | 
				
			||||||
 | 
					#include <drogon/utils/Utilities.h>
 | 
				
			||||||
 | 
					#include <trantor/net/EventLoopThread.h>
 | 
				
			||||||
 | 
					#include <trantor/net/TcpClient.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using namespace drogon;
 | 
				
			||||||
 | 
					using namespace trantor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct EMail {
 | 
				
			||||||
 | 
					  enum states {
 | 
				
			||||||
 | 
					    Init,
 | 
				
			||||||
 | 
					    HandShake,
 | 
				
			||||||
 | 
					    Tls,
 | 
				
			||||||
 | 
					    Auth,
 | 
				
			||||||
 | 
					    User,
 | 
				
			||||||
 | 
					    Pass,
 | 
				
			||||||
 | 
					    Mail,
 | 
				
			||||||
 | 
					    Rcpt,
 | 
				
			||||||
 | 
					    Data,
 | 
				
			||||||
 | 
					    Body,
 | 
				
			||||||
 | 
					    Quit,
 | 
				
			||||||
 | 
					    Close
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  std::string m_from;
 | 
				
			||||||
 | 
					  std::string m_to;
 | 
				
			||||||
 | 
					  std::string m_subject;
 | 
				
			||||||
 | 
					  std::string m_content;
 | 
				
			||||||
 | 
					  std::string m_user;
 | 
				
			||||||
 | 
					  std::string m_passwd;
 | 
				
			||||||
 | 
					  states m_status;
 | 
				
			||||||
 | 
					  std::string m_uuid;
 | 
				
			||||||
 | 
					  bool m_isHTML{false};
 | 
				
			||||||
 | 
					  std::shared_ptr<trantor::TcpClient> m_socket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  EMail(std::string from, std::string to, std::string subject,
 | 
				
			||||||
 | 
					        std::string content, std::string user, std::string passwd, bool isHTML,
 | 
				
			||||||
 | 
					        std::shared_ptr<trantor::TcpClient> socket)
 | 
				
			||||||
 | 
					      : m_from(std::move(from)), m_to(std::move(to)),
 | 
				
			||||||
 | 
					        m_subject(std::move(subject)), m_content(std::move(content)),
 | 
				
			||||||
 | 
					        m_user(std::move(user)), m_passwd(std::move(passwd)),
 | 
				
			||||||
 | 
					        m_socket(std::move(socket)), m_isHTML(isHTML),
 | 
				
			||||||
 | 
					        m_uuid(drogon::utils::getUuid()) {
 | 
				
			||||||
 | 
					    m_status = Init;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ~EMail() = default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static std::unordered_map<std::string, std::shared_ptr<EMail>>
 | 
				
			||||||
 | 
					      m_emails; // Container for processing emails
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::unordered_map<std::string, std::shared_ptr<EMail>> EMail::m_emails;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void SMTPMail::initAndStart(const Json::Value &config) {
 | 
				
			||||||
 | 
					  /// Initialize and start the plugin
 | 
				
			||||||
 | 
					  LOG_INFO << "SMTPMail initialized and started";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void SMTPMail::shutdown() {
 | 
				
			||||||
 | 
					  /// Shutdown the plugin
 | 
				
			||||||
 | 
					  LOG_INFO << "STMPMail shutdown";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void messagesHandle(const trantor::TcpConnectionPtr &connPtr,
 | 
				
			||||||
 | 
					                    trantor::MsgBuffer *msg,
 | 
				
			||||||
 | 
					                    const std::shared_ptr<EMail> &email,
 | 
				
			||||||
 | 
					                    const std::function<void(const std::string &msg)> &cb) {
 | 
				
			||||||
 | 
					  std::string receivedMsg;
 | 
				
			||||||
 | 
					  while (msg->readableBytes() > 0) {
 | 
				
			||||||
 | 
					    std::string buf(msg->peek(), msg->readableBytes());
 | 
				
			||||||
 | 
					    receivedMsg.append(buf);
 | 
				
			||||||
 | 
					    //        LOG_INFO << buf;
 | 
				
			||||||
 | 
					    msg->retrieveAll();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  LOG_TRACE << "receive: " << receivedMsg;
 | 
				
			||||||
 | 
					  std::string responseCode(receivedMsg.begin(), receivedMsg.begin() + 3);
 | 
				
			||||||
 | 
					  //    std::string responseMsg(receivedMsg.begin() + 4, receivedMsg.end());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (email->m_status == EMail::Init && responseCode == "220") {
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append("EHLO smtpclient.qw");
 | 
				
			||||||
 | 
					    outMsg.append("\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->send(std::move(out));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::HandShake;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::HandShake && responseCode == "220") {
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append("EHLO smtpclient.qw");
 | 
				
			||||||
 | 
					    outMsg.append("\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->startClientEncryption(
 | 
				
			||||||
 | 
					        [connPtr, out]() {
 | 
				
			||||||
 | 
					          // LOG_TRACE << "SSL established";
 | 
				
			||||||
 | 
					          connPtr->send(out);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        false, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::Auth;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::HandShake && responseCode == "250") {
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append("STARTTLS");
 | 
				
			||||||
 | 
					    outMsg.append("\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->send(std::move(out));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::HandShake;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::Auth && responseCode == "250") {
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append("AUTH LOGIN");
 | 
				
			||||||
 | 
					    outMsg.append("\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->send(std::move(out));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::User;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::User && responseCode == "334") {
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    std::string secret(email->m_user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // outMsg.append(base64_encode(reinterpret_cast<const unsigned
 | 
				
			||||||
 | 
					    // char*>(secret.c_str()), secret.length()));
 | 
				
			||||||
 | 
					    outMsg.append(drogon::utils::base64Encode(
 | 
				
			||||||
 | 
					        reinterpret_cast<const unsigned char *>(secret.c_str()),
 | 
				
			||||||
 | 
					        secret.length()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append("\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->send(std::move(out));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::Pass;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::Pass && responseCode == "334") {
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    std::string secret(email->m_passwd);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append(drogon::utils::base64Encode(
 | 
				
			||||||
 | 
					        reinterpret_cast<const unsigned char *>(secret.c_str()),
 | 
				
			||||||
 | 
					        secret.length()));
 | 
				
			||||||
 | 
					    outMsg.append("\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->send(std::move(out));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::Mail;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::Mail && responseCode == "235") {
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append("MAIL FROM:<");
 | 
				
			||||||
 | 
					    outMsg.append(email->m_from);
 | 
				
			||||||
 | 
					    outMsg.append(">\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->send(std::move(out));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::Rcpt;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::Rcpt && responseCode == "250") {
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append("RCPT TO:<");
 | 
				
			||||||
 | 
					    outMsg.append(email->m_to);
 | 
				
			||||||
 | 
					    outMsg.append(">\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->send(std::move(out));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::Data;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::Data && responseCode == "250") {
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append("DATA");
 | 
				
			||||||
 | 
					    outMsg.append("\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->send(std::move(out));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::Body;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::Body && responseCode == "354") {
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					    std::time_t t = std::time(nullptr);
 | 
				
			||||||
 | 
					    char buf[100];
 | 
				
			||||||
 | 
					    std::strftime(buf, 100, "%a, %d %b %Y %T %z",  std::localtime(&t));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append("To: " + email->m_to + "\r\n");
 | 
				
			||||||
 | 
					    outMsg.append("From: " + email->m_from + "\r\n");
 | 
				
			||||||
 | 
					    outMsg.append("Date: " + std::string(buf) + "\r\n");
 | 
				
			||||||
 | 
					    if (email->m_isHTML) {
 | 
				
			||||||
 | 
					      outMsg.append("Content-Type: text/html;\r\n");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    outMsg.append("Subject: " + email->m_subject + "\r\n\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append(email->m_content);
 | 
				
			||||||
 | 
					    outMsg.append("\r\n.\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->send(std::move(out));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::Quit;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::Quit && responseCode == "250") {
 | 
				
			||||||
 | 
					    trantor::MsgBuffer out;
 | 
				
			||||||
 | 
					    std::string outMsg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    outMsg.append("QUIT");
 | 
				
			||||||
 | 
					    outMsg.append("\r\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.append(outMsg.data(), outMsg.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connPtr->send(std::move(out));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email->m_status = EMail::Close;
 | 
				
			||||||
 | 
					  } else if (email->m_status == EMail::Close) {
 | 
				
			||||||
 | 
					    /*Callback here for succeed delivery is probable*/
 | 
				
			||||||
 | 
					    cb("EMail sent. ID : " + email->m_uuid);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    email->m_status = EMail::Close;
 | 
				
			||||||
 | 
					    /*Callback here for notification is probable*/
 | 
				
			||||||
 | 
					    cb(receivedMsg);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::string
 | 
				
			||||||
 | 
					SMTPMail::sendEmail(const std::string &mailServer, const uint16_t &port,
 | 
				
			||||||
 | 
					                    const std::string &from, const std::string &to,
 | 
				
			||||||
 | 
					                    const std::string &subject, const std::string &content,
 | 
				
			||||||
 | 
					                    const std::string &user, const std::string &passwd,
 | 
				
			||||||
 | 
					                    bool isHTML,
 | 
				
			||||||
 | 
					                    const std::function<void(const std::string &)> &cb) {
 | 
				
			||||||
 | 
					  if (mailServer.empty() || from.empty() || to.empty() || subject.empty() ||
 | 
				
			||||||
 | 
					      user.empty() || passwd.empty()) {
 | 
				
			||||||
 | 
					    LOG_WARN << "Invalid input(s) - "
 | 
				
			||||||
 | 
					             << "\nServer : " << mailServer << "\nPort : " << port
 | 
				
			||||||
 | 
					             << "\nfrom : " << from << "\nto : " << to
 | 
				
			||||||
 | 
					             << "\nsubject : " << subject << "\nuser : " << user
 | 
				
			||||||
 | 
					             << "\npasswd : " << passwd;
 | 
				
			||||||
 | 
					    return {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static auto hasLineBreak = [](const std::string &msg) {
 | 
				
			||||||
 | 
					    if (std::string::npos != msg.find_first_of("\n") ||
 | 
				
			||||||
 | 
					        std::string::npos != msg.find_first_of("\r")) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (hasLineBreak(from)) {
 | 
				
			||||||
 | 
					    LOG_WARN << "Invalid \"FROM\" data : " << from;
 | 
				
			||||||
 | 
					    return {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (hasLineBreak(to)) {
 | 
				
			||||||
 | 
					    LOG_WARN << "Invalid \"TO\" data : " << to;
 | 
				
			||||||
 | 
					    return {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (hasLineBreak(subject)) {
 | 
				
			||||||
 | 
					    LOG_WARN << "Invalid \"SUBJECT\" data : " << subject.data();
 | 
				
			||||||
 | 
					    return {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  LOG_TRACE << "New TcpClient : " << mailServer << ":" << port;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Create the email
 | 
				
			||||||
 | 
					  auto email = std::make_shared<EMail>(from, to, subject, content, user, passwd,
 | 
				
			||||||
 | 
					                                       isHTML, nullptr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  auto resolver = app().getResolver();
 | 
				
			||||||
 | 
					  resolver->resolve(
 | 
				
			||||||
 | 
					      mailServer, [email, port, cb](const trantor::InetAddress &addr) {
 | 
				
			||||||
 | 
					        constexpr size_t defaultLoopIdA = 10;
 | 
				
			||||||
 | 
					        constexpr size_t defaultLoopIdB = 9;
 | 
				
			||||||
 | 
					        auto loopA = app().getIOLoop(defaultLoopIdA);
 | 
				
			||||||
 | 
					        auto loopB = app().getIOLoop(defaultLoopIdB);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if ( loopA == loopB ) {
 | 
				
			||||||
 | 
					          LOG_WARN << "Please provide at least 2 threads for this plugin";
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        auto loop = loopA->isInLoopThread() ? loopB : loopA;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        assert(loop);                    // Should never be null
 | 
				
			||||||
 | 
					        trantor::InetAddress addr_(addr.toIp(), port, false);
 | 
				
			||||||
 | 
					        auto tcpSocket =
 | 
				
			||||||
 | 
					            std::make_shared<trantor::TcpClient>(loop, addr_, "SMTPMail");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        email->m_socket = tcpSocket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        std::weak_ptr<EMail> email_wptr = email;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        EMail::m_emails.emplace(email->m_uuid,
 | 
				
			||||||
 | 
					                                email); // Assuming there is no uuid collision
 | 
				
			||||||
 | 
					        tcpSocket->setConnectionCallback(
 | 
				
			||||||
 | 
					            [email_wptr](const trantor::TcpConnectionPtr &connPtr) {
 | 
				
			||||||
 | 
					              auto email_ptr = email_wptr.lock();
 | 
				
			||||||
 | 
					              if (!email_ptr) {
 | 
				
			||||||
 | 
					                LOG_WARN << "EMail pointer gone";
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              if (connPtr->connected()) {
 | 
				
			||||||
 | 
					                // send request;
 | 
				
			||||||
 | 
					                LOG_TRACE << "Connection established!";
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                LOG_TRACE << "Connection disconnect";
 | 
				
			||||||
 | 
					                EMail::m_emails.erase(
 | 
				
			||||||
 | 
					                    email_ptr->m_uuid); // Remove the email in list
 | 
				
			||||||
 | 
					                // thisPtr->onError(std::string("ReqResult::NetworkFailure"));
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        tcpSocket->setConnectionErrorCallback([email_wptr]() {
 | 
				
			||||||
 | 
					          auto email_ptr = email_wptr.lock();
 | 
				
			||||||
 | 
					          if (!email_ptr) {
 | 
				
			||||||
 | 
					            LOG_ERROR << "EMail pointer gone";
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          // can't connect to server
 | 
				
			||||||
 | 
					          LOG_ERROR << "Bad Server address";
 | 
				
			||||||
 | 
					          EMail::m_emails.erase(email_ptr->m_uuid); // Remove the email in list
 | 
				
			||||||
 | 
					          // thisPtr->onError(std::string("ReqResult::BadServerAddress"));
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        auto cb_(cb ? cb : [](const std::string &msg) {
 | 
				
			||||||
 | 
					          LOG_INFO << "Default email callback : " << msg;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        tcpSocket->setMessageCallback(
 | 
				
			||||||
 | 
					            [email_wptr, cb_](const trantor::TcpConnectionPtr &connPtr,
 | 
				
			||||||
 | 
					                              trantor::MsgBuffer *msg) {
 | 
				
			||||||
 | 
					              auto email_ptr = email_wptr.lock();
 | 
				
			||||||
 | 
					              if (!email_ptr) {
 | 
				
			||||||
 | 
					                LOG_ERROR << "EMail pointer gone";
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              // email->m_socket->disconnect();
 | 
				
			||||||
 | 
					              messagesHandle(connPtr, msg, email_ptr, cb_);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        tcpSocket->connect(); // Start trying to send the email
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  return email->m_uuid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										63
									
								
								backend/SMTPMail-drogon-master/SMTPMail.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								backend/SMTPMail-drogon-master/SMTPMail.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  SMTPMail.h
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This plugin is for SMTP mail delievery for the Drogon web-framework.
 | 
				
			||||||
 | 
					Implementation
 | 
				
			||||||
 | 
					 * reference from the project "SMTPClient" with Qt5 by kelvins. Please check out
 | 
				
			||||||
 | 
					 * https://github.com/kelvins/SMTPClient.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright 2020 ihmc3jn09hk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy of
 | 
				
			||||||
 | 
					this software and associated documentation files (the "Software"), to deal in
 | 
				
			||||||
 | 
					the Software without restriction, including without limitation the rights to
 | 
				
			||||||
 | 
					use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 | 
				
			||||||
 | 
					the Software, and to permit persons to whom the Software is furnished to do so,
 | 
				
			||||||
 | 
					subject to the following conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all
 | 
				
			||||||
 | 
					copies or substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
				
			||||||
 | 
					IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 | 
				
			||||||
 | 
					FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 | 
				
			||||||
 | 
					COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 | 
				
			||||||
 | 
					IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 | 
				
			||||||
 | 
					CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <drogon/plugins/Plugin.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SMTPMail : public drogon::Plugin<SMTPMail> {
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
					  SMTPMail() = default;
 | 
				
			||||||
 | 
					  /// This method must be called by drogon to initialize and start the plugin.
 | 
				
			||||||
 | 
					  /// It must be implemented by the user.
 | 
				
			||||||
 | 
					  void initAndStart(const Json::Value &config) override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// This method must be called by drogon to shutdown the plugin.
 | 
				
			||||||
 | 
					  /// It must be implemented by the user.
 | 
				
			||||||
 | 
					  void shutdown() override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** Send an email
 | 
				
			||||||
 | 
					   * return : An ID of the email.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  std::string sendEmail(
 | 
				
			||||||
 | 
					      const std::string
 | 
				
			||||||
 | 
					          &mailServer, // Mail server address/dns E.g. 127.0.0.1/smtp.mail.com
 | 
				
			||||||
 | 
					      const uint16_t &port,       // Port  E.g. 587
 | 
				
			||||||
 | 
					      const std::string &from,    // Send from whom E.g. drogon@gmail.com
 | 
				
			||||||
 | 
					      const std::string &to,      // Reciever       E.g. drogon@yahoo.com
 | 
				
			||||||
 | 
					      const std::string &subject, // The email title/subject
 | 
				
			||||||
 | 
					      const std::string &content, // The email content.
 | 
				
			||||||
 | 
					      const std::string &user,    // User      (Usually same as "from")
 | 
				
			||||||
 | 
					      const std::string &passwd,  // Password
 | 
				
			||||||
 | 
					      bool isHTML,                // content type
 | 
				
			||||||
 | 
					      const std::function<void(const std::string &)> &cb = {}
 | 
				
			||||||
 | 
					      // The callback for email sent notification
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -6,7 +6,7 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "Inode.h"
 | 
					#include "Inode.h"
 | 
				
			||||||
#include <drogon/utils/Utilities.h>
 | 
					#include "drogon/utils/Utilities.h"
 | 
				
			||||||
#include <string>
 | 
					#include <string>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using namespace drogon;
 | 
					using namespace drogon;
 | 
				
			||||||
@@ -19,6 +19,7 @@ const std::string Inode::Cols::_name = "name";
 | 
				
			|||||||
const std::string Inode::Cols::_parent_id = "parent_id";
 | 
					const std::string Inode::Cols::_parent_id = "parent_id";
 | 
				
			||||||
const std::string Inode::Cols::_owner_id = "owner_id";
 | 
					const std::string Inode::Cols::_owner_id = "owner_id";
 | 
				
			||||||
const std::string Inode::Cols::_size = "size";
 | 
					const std::string Inode::Cols::_size = "size";
 | 
				
			||||||
 | 
					const std::string Inode::Cols::_has_preview = "has_preview";
 | 
				
			||||||
const std::string Inode::primaryKeyName = "id";
 | 
					const std::string Inode::primaryKeyName = "id";
 | 
				
			||||||
const bool Inode::hasPrimaryKey = true;
 | 
					const bool Inode::hasPrimaryKey = true;
 | 
				
			||||||
const std::string Inode::tableName = "inode";
 | 
					const std::string Inode::tableName = "inode";
 | 
				
			||||||
@@ -29,7 +30,8 @@ const std::vector<typename Inode::MetaData> Inode::metaData_={
 | 
				
			|||||||
{"name","std::string","text",0,0,0,0},
 | 
					{"name","std::string","text",0,0,0,0},
 | 
				
			||||||
{"parent_id","uint64_t","integer",8,0,0,0},
 | 
					{"parent_id","uint64_t","integer",8,0,0,0},
 | 
				
			||||||
{"owner_id","uint64_t","integer",8,0,0,1},
 | 
					{"owner_id","uint64_t","integer",8,0,0,1},
 | 
				
			||||||
{"size","uint64_t","integer",8,0,0,0}
 | 
					{"size","uint64_t","integer",8,0,0,0},
 | 
				
			||||||
 | 
					{"has_preview","uint64_t","integer",8,0,0,1}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
const std::string &Inode::getColumnName(size_t index) noexcept(false)
 | 
					const std::string &Inode::getColumnName(size_t index) noexcept(false)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -64,11 +66,15 @@ Inode::Inode(const Row &r, const ssize_t indexOffset) noexcept
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            size_=std::make_shared<uint64_t>(r["size"].as<uint64_t>());
 | 
					            size_=std::make_shared<uint64_t>(r["size"].as<uint64_t>());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if(!r["has_preview"].isNull())
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            hasPreview_=std::make_shared<uint64_t>(r["has_preview"].as<uint64_t>());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        size_t offset = (size_t)indexOffset;
 | 
					        size_t offset = (size_t)indexOffset;
 | 
				
			||||||
        if(offset + 6 > r.size())
 | 
					        if(offset + 7 > r.size())
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            LOG_FATAL << "Invalid SQL result for this model";
 | 
					            LOG_FATAL << "Invalid SQL result for this model";
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
@@ -104,13 +110,18 @@ Inode::Inode(const Row &r, const ssize_t indexOffset) noexcept
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            size_=std::make_shared<uint64_t>(r[index].as<uint64_t>());
 | 
					            size_=std::make_shared<uint64_t>(r[index].as<uint64_t>());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        index = offset + 6;
 | 
				
			||||||
 | 
					        if(!r[index].isNull())
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            hasPreview_=std::make_shared<uint64_t>(r[index].as<uint64_t>());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Inode::Inode(const Json::Value &pJson, const std::vector<std::string> &pMasqueradingVector) noexcept(false)
 | 
					Inode::Inode(const Json::Value &pJson, const std::vector<std::string> &pMasqueradingVector) noexcept(false)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    if(pMasqueradingVector.size() != 6)
 | 
					    if(pMasqueradingVector.size() != 7)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        LOG_ERROR << "Bad masquerading vector";
 | 
					        LOG_ERROR << "Bad masquerading vector";
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
@@ -163,6 +174,14 @@ Inode::Inode(const Json::Value &pJson, const std::vector<std::string> &pMasquera
 | 
				
			|||||||
            size_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[5]].asUInt64());
 | 
					            size_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[5]].asUInt64());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(!pMasqueradingVector[6].empty() && pJson.isMember(pMasqueradingVector[6]))
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        dirtyFlag_[6] = true;
 | 
				
			||||||
 | 
					        if(!pJson[pMasqueradingVector[6]].isNull())
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            hasPreview_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[6]].asUInt64());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Inode::Inode(const Json::Value &pJson) noexcept(false)
 | 
					Inode::Inode(const Json::Value &pJson) noexcept(false)
 | 
				
			||||||
@@ -215,12 +234,20 @@ Inode::Inode(const Json::Value &pJson) noexcept(false)
 | 
				
			|||||||
            size_=std::make_shared<uint64_t>((uint64_t)pJson["size"].asUInt64());
 | 
					            size_=std::make_shared<uint64_t>((uint64_t)pJson["size"].asUInt64());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(pJson.isMember("has_preview"))
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        dirtyFlag_[6]=true;
 | 
				
			||||||
 | 
					        if(!pJson["has_preview"].isNull())
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            hasPreview_=std::make_shared<uint64_t>((uint64_t)pJson["has_preview"].asUInt64());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void Inode::updateByMasqueradedJson(const Json::Value &pJson,
 | 
					void Inode::updateByMasqueradedJson(const Json::Value &pJson,
 | 
				
			||||||
                                            const std::vector<std::string> &pMasqueradingVector) noexcept(false)
 | 
					                                            const std::vector<std::string> &pMasqueradingVector) noexcept(false)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    if(pMasqueradingVector.size() != 6)
 | 
					    if(pMasqueradingVector.size() != 7)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        LOG_ERROR << "Bad masquerading vector";
 | 
					        LOG_ERROR << "Bad masquerading vector";
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
@@ -272,6 +299,14 @@ void Inode::updateByMasqueradedJson(const Json::Value &pJson,
 | 
				
			|||||||
            size_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[5]].asUInt64());
 | 
					            size_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[5]].asUInt64());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(!pMasqueradingVector[6].empty() && pJson.isMember(pMasqueradingVector[6]))
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        dirtyFlag_[6] = true;
 | 
				
			||||||
 | 
					        if(!pJson[pMasqueradingVector[6]].isNull())
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            hasPreview_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[6]].asUInt64());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void Inode::updateByJson(const Json::Value &pJson) noexcept(false)
 | 
					void Inode::updateByJson(const Json::Value &pJson) noexcept(false)
 | 
				
			||||||
@@ -323,6 +358,14 @@ void Inode::updateByJson(const Json::Value &pJson) noexcept(false)
 | 
				
			|||||||
            size_=std::make_shared<uint64_t>((uint64_t)pJson["size"].asUInt64());
 | 
					            size_=std::make_shared<uint64_t>((uint64_t)pJson["size"].asUInt64());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(pJson.isMember("has_preview"))
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        dirtyFlag_[6] = true;
 | 
				
			||||||
 | 
					        if(!pJson["has_preview"].isNull())
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            hasPreview_=std::make_shared<uint64_t>((uint64_t)pJson["has_preview"].asUInt64());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const uint64_t &Inode::getValueOfId() const noexcept
 | 
					const uint64_t &Inode::getValueOfId() const noexcept
 | 
				
			||||||
@@ -452,6 +495,23 @@ void Inode::setSizeToNull() noexcept
 | 
				
			|||||||
    dirtyFlag_[5] = true;
 | 
					    dirtyFlag_[5] = true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uint64_t &Inode::getValueOfHasPreview() const noexcept
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    const static uint64_t defaultValue = uint64_t();
 | 
				
			||||||
 | 
					    if(hasPreview_)
 | 
				
			||||||
 | 
					        return *hasPreview_;
 | 
				
			||||||
 | 
					    return defaultValue;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const std::shared_ptr<uint64_t> &Inode::getHasPreview() const noexcept
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    return hasPreview_;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					void Inode::setHasPreview(const uint64_t &pHasPreview) noexcept
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    hasPreview_ = std::make_shared<uint64_t>(pHasPreview);
 | 
				
			||||||
 | 
					    dirtyFlag_[6] = true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void Inode::updateId(const uint64_t id)
 | 
					void Inode::updateId(const uint64_t id)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    id_ = std::make_shared<uint64_t>(id);
 | 
					    id_ = std::make_shared<uint64_t>(id);
 | 
				
			||||||
@@ -464,7 +524,8 @@ const std::vector<std::string> &Inode::insertColumns() noexcept
 | 
				
			|||||||
        "name",
 | 
					        "name",
 | 
				
			||||||
        "parent_id",
 | 
					        "parent_id",
 | 
				
			||||||
        "owner_id",
 | 
					        "owner_id",
 | 
				
			||||||
        "size"
 | 
					        "size",
 | 
				
			||||||
 | 
					        "has_preview"
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    return inCols;
 | 
					    return inCols;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -526,6 +587,17 @@ void Inode::outputArgs(drogon::orm::internal::SqlBinder &binder) const
 | 
				
			|||||||
            binder << nullptr;
 | 
					            binder << nullptr;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(dirtyFlag_[6])
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if(getHasPreview())
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            binder << getValueOfHasPreview();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            binder << nullptr;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const std::vector<std::string> Inode::updateColumns() const
 | 
					const std::vector<std::string> Inode::updateColumns() const
 | 
				
			||||||
@@ -551,6 +623,10 @@ const std::vector<std::string> Inode::updateColumns() const
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        ret.push_back(getColumnName(5));
 | 
					        ret.push_back(getColumnName(5));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(dirtyFlag_[6])
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        ret.push_back(getColumnName(6));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return ret;
 | 
					    return ret;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -611,6 +687,17 @@ void Inode::updateArgs(drogon::orm::internal::SqlBinder &binder) const
 | 
				
			|||||||
            binder << nullptr;
 | 
					            binder << nullptr;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(dirtyFlag_[6])
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if(getHasPreview())
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            binder << getValueOfHasPreview();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            binder << nullptr;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
Json::Value Inode::toJson() const
 | 
					Json::Value Inode::toJson() const
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -663,6 +750,14 @@ Json::Value Inode::toJson() const
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        ret["size"]=Json::Value();
 | 
					        ret["size"]=Json::Value();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(getHasPreview())
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        ret["has_preview"]=(Json::UInt64)getValueOfHasPreview();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        ret["has_preview"]=Json::Value();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return ret;
 | 
					    return ret;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -670,7 +765,7 @@ Json::Value Inode::toMasqueradedJson(
 | 
				
			|||||||
    const std::vector<std::string> &pMasqueradingVector) const
 | 
					    const std::vector<std::string> &pMasqueradingVector) const
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    Json::Value ret;
 | 
					    Json::Value ret;
 | 
				
			||||||
    if(pMasqueradingVector.size() == 6)
 | 
					    if(pMasqueradingVector.size() == 7)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if(!pMasqueradingVector[0].empty())
 | 
					        if(!pMasqueradingVector[0].empty())
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -738,6 +833,17 @@ Json::Value Inode::toMasqueradedJson(
 | 
				
			|||||||
                ret[pMasqueradingVector[5]]=Json::Value();
 | 
					                ret[pMasqueradingVector[5]]=Json::Value();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if(!pMasqueradingVector[6].empty())
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if(getHasPreview())
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ret[pMasqueradingVector[6]]=(Json::UInt64)getValueOfHasPreview();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ret[pMasqueradingVector[6]]=Json::Value();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return ret;
 | 
					        return ret;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    LOG_ERROR << "Masquerade failed";
 | 
					    LOG_ERROR << "Masquerade failed";
 | 
				
			||||||
@@ -789,6 +895,14 @@ Json::Value Inode::toMasqueradedJson(
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        ret["size"]=Json::Value();
 | 
					        ret["size"]=Json::Value();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(getHasPreview())
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        ret["has_preview"]=(Json::UInt64)getValueOfHasPreview();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        ret["has_preview"]=Json::Value();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return ret;
 | 
					    return ret;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -834,13 +948,23 @@ bool Inode::validateJsonForCreation(const Json::Value &pJson, std::string &err)
 | 
				
			|||||||
        if(!validJsonOfField(5, "size", pJson["size"], err, true))
 | 
					        if(!validJsonOfField(5, "size", pJson["size"], err, true))
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(pJson.isMember("has_preview"))
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if(!validJsonOfField(6, "has_preview", pJson["has_preview"], err, true))
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        err="The has_preview column cannot be null";
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
bool Inode::validateMasqueradedJsonForCreation(const Json::Value &pJson,
 | 
					bool Inode::validateMasqueradedJsonForCreation(const Json::Value &pJson,
 | 
				
			||||||
                                               const std::vector<std::string> &pMasqueradingVector,
 | 
					                                               const std::vector<std::string> &pMasqueradingVector,
 | 
				
			||||||
                                               std::string &err)
 | 
					                                               std::string &err)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    if(pMasqueradingVector.size() != 6)
 | 
					    if(pMasqueradingVector.size() != 7)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        err = "Bad masquerading vector";
 | 
					        err = "Bad masquerading vector";
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
@@ -904,6 +1028,19 @@ bool Inode::validateMasqueradedJsonForCreation(const Json::Value &pJson,
 | 
				
			|||||||
                  return false;
 | 
					                  return false;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      if(!pMasqueradingVector[6].empty())
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					          if(pJson.isMember(pMasqueradingVector[6]))
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					              if(!validJsonOfField(6, pMasqueradingVector[6], pJson[pMasqueradingVector[6]], err, true))
 | 
				
			||||||
 | 
					                  return false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            err="The " + pMasqueradingVector[6] + " column cannot be null";
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    catch(const Json::LogicError &e)
 | 
					    catch(const Json::LogicError &e)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -949,13 +1086,18 @@ bool Inode::validateJsonForUpdate(const Json::Value &pJson, std::string &err)
 | 
				
			|||||||
        if(!validJsonOfField(5, "size", pJson["size"], err, false))
 | 
					        if(!validJsonOfField(5, "size", pJson["size"], err, false))
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if(pJson.isMember("has_preview"))
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if(!validJsonOfField(6, "has_preview", pJson["has_preview"], err, false))
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
bool Inode::validateMasqueradedJsonForUpdate(const Json::Value &pJson,
 | 
					bool Inode::validateMasqueradedJsonForUpdate(const Json::Value &pJson,
 | 
				
			||||||
                                             const std::vector<std::string> &pMasqueradingVector,
 | 
					                                             const std::vector<std::string> &pMasqueradingVector,
 | 
				
			||||||
                                             std::string &err)
 | 
					                                             std::string &err)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    if(pMasqueradingVector.size() != 6)
 | 
					    if(pMasqueradingVector.size() != 7)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        err = "Bad masquerading vector";
 | 
					        err = "Bad masquerading vector";
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
@@ -996,6 +1138,11 @@ bool Inode::validateMasqueradedJsonForUpdate(const Json::Value &pJson,
 | 
				
			|||||||
          if(!validJsonOfField(5, pMasqueradingVector[5], pJson[pMasqueradingVector[5]], err, false))
 | 
					          if(!validJsonOfField(5, pMasqueradingVector[5], pJson[pMasqueradingVector[5]], err, false))
 | 
				
			||||||
              return false;
 | 
					              return false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      if(!pMasqueradingVector[6].empty() && pJson.isMember(pMasqueradingVector[6]))
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					          if(!validJsonOfField(6, pMasqueradingVector[6], pJson[pMasqueradingVector[6]], err, false))
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    catch(const Json::LogicError &e)
 | 
					    catch(const Json::LogicError &e)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -1086,6 +1233,18 @@ bool Inode::validJsonOfField(size_t index,
 | 
				
			|||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
 | 
					        case 6:
 | 
				
			||||||
 | 
					            if(pJson.isNull())
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                err="The " + fieldName + " column cannot be null";
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if(!pJson.isUInt64())
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                err="Type error in the "+fieldName+" field";
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
        default:
 | 
					        default:
 | 
				
			||||||
            err="Internal error in the server";
 | 
					            err="Internal error in the server";
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
@@ -6,17 +6,17 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pragma once
 | 
					#pragma once
 | 
				
			||||||
#include <drogon/orm/Result.h>
 | 
					#include "drogon/orm/Result.h"
 | 
				
			||||||
#include <drogon/orm/Row.h>
 | 
					#include "drogon/orm/Row.h"
 | 
				
			||||||
#include <drogon/orm/Field.h>
 | 
					#include "drogon/orm/Field.h"
 | 
				
			||||||
#include <drogon/orm/SqlBinder.h>
 | 
					#include "drogon/orm/SqlBinder.h"
 | 
				
			||||||
#include <drogon/orm/Mapper.h>
 | 
					#include "drogon/orm/Mapper.h"
 | 
				
			||||||
#ifdef __cpp_impl_coroutine
 | 
					#ifdef __cpp_impl_coroutine
 | 
				
			||||||
#include <drogon/orm/CoroMapper.h>
 | 
					#include <drogon/orm/CoroMapper.h>
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
#include <trantor/utils/Date.h>
 | 
					#include "trantor/utils/Date.h"
 | 
				
			||||||
#include <trantor/utils/Logger.h>
 | 
					#include "trantor/utils/Logger.h"
 | 
				
			||||||
#include <json/json.h>
 | 
					#include "json/json.h"
 | 
				
			||||||
#include <string>
 | 
					#include <string>
 | 
				
			||||||
#include <memory>
 | 
					#include <memory>
 | 
				
			||||||
#include <vector>
 | 
					#include <vector>
 | 
				
			||||||
@@ -48,6 +48,7 @@ class Inode
 | 
				
			|||||||
        static const std::string _parent_id;
 | 
					        static const std::string _parent_id;
 | 
				
			||||||
        static const std::string _owner_id;
 | 
					        static const std::string _owner_id;
 | 
				
			||||||
        static const std::string _size;
 | 
					        static const std::string _size;
 | 
				
			||||||
 | 
					        static const std::string _has_preview;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const static int primaryKeyNumber;
 | 
					    const static int primaryKeyNumber;
 | 
				
			||||||
@@ -151,8 +152,16 @@ class Inode
 | 
				
			|||||||
    void setSize(const uint64_t &pSize) noexcept;
 | 
					    void setSize(const uint64_t &pSize) noexcept;
 | 
				
			||||||
    void setSizeToNull() noexcept;
 | 
					    void setSizeToNull() noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**  For column has_preview  */
 | 
				
			||||||
 | 
					    ///Get the value of the column has_preview, returns the default value if the column is null
 | 
				
			||||||
 | 
					    const uint64_t &getValueOfHasPreview() const noexcept;
 | 
				
			||||||
 | 
					    ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
 | 
				
			||||||
 | 
					    const std::shared_ptr<uint64_t> &getHasPreview() const noexcept;
 | 
				
			||||||
 | 
					    ///Set the value of the column has_preview
 | 
				
			||||||
 | 
					    void setHasPreview(const uint64_t &pHasPreview) noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static size_t getColumnNumber() noexcept {  return 6;  }
 | 
					
 | 
				
			||||||
 | 
					    static size_t getColumnNumber() noexcept {  return 7;  }
 | 
				
			||||||
    static const std::string &getColumnName(size_t index) noexcept(false);
 | 
					    static const std::string &getColumnName(size_t index) noexcept(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Json::Value toJson() const;
 | 
					    Json::Value toJson() const;
 | 
				
			||||||
@@ -175,6 +184,7 @@ class Inode
 | 
				
			|||||||
    std::shared_ptr<uint64_t> parentId_;
 | 
					    std::shared_ptr<uint64_t> parentId_;
 | 
				
			||||||
    std::shared_ptr<uint64_t> ownerId_;
 | 
					    std::shared_ptr<uint64_t> ownerId_;
 | 
				
			||||||
    std::shared_ptr<uint64_t> size_;
 | 
					    std::shared_ptr<uint64_t> size_;
 | 
				
			||||||
 | 
					    std::shared_ptr<uint64_t> hasPreview_;
 | 
				
			||||||
    struct MetaData
 | 
					    struct MetaData
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        const std::string colName_;
 | 
					        const std::string colName_;
 | 
				
			||||||
@@ -186,7 +196,7 @@ class Inode
 | 
				
			|||||||
        const bool notNull_;
 | 
					        const bool notNull_;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    static const std::vector<MetaData> metaData_;
 | 
					    static const std::vector<MetaData> metaData_;
 | 
				
			||||||
    bool dirtyFlag_[6]={ false };
 | 
					    bool dirtyFlag_[7]={ false };
 | 
				
			||||||
  public:
 | 
					  public:
 | 
				
			||||||
    static const std::string &sqlForFindingByPrimaryKey()
 | 
					    static const std::string &sqlForFindingByPrimaryKey()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -229,6 +239,11 @@ class Inode
 | 
				
			|||||||
            sql += "size,";
 | 
					            sql += "size,";
 | 
				
			||||||
            ++parametersCount;
 | 
					            ++parametersCount;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if(dirtyFlag_[6])
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            sql += "has_preview,";
 | 
				
			||||||
 | 
					            ++parametersCount;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        if(parametersCount > 0)
 | 
					        if(parametersCount > 0)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            sql[sql.length()-1]=')';
 | 
					            sql[sql.length()-1]=')';
 | 
				
			||||||
@@ -261,6 +276,11 @@ class Inode
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            sql.append("?,");
 | 
					            sql.append("?,");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if(dirtyFlag_[6])
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            sql.append("?,");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if(parametersCount > 0)
 | 
					        if(parametersCount > 0)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -6,7 +6,7 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "Tokens.h"
 | 
					#include "Tokens.h"
 | 
				
			||||||
#include <drogon/utils/Utilities.h>
 | 
					#include "drogon/utils/Utilities.h"
 | 
				
			||||||
#include <string>
 | 
					#include <string>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using namespace drogon;
 | 
					using namespace drogon;
 | 
				
			||||||
@@ -6,17 +6,17 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pragma once
 | 
					#pragma once
 | 
				
			||||||
#include <drogon/orm/Result.h>
 | 
					#include "drogon/orm/Result.h"
 | 
				
			||||||
#include <drogon/orm/Row.h>
 | 
					#include "drogon/orm/Row.h"
 | 
				
			||||||
#include <drogon/orm/Field.h>
 | 
					#include "drogon/orm/Field.h"
 | 
				
			||||||
#include <drogon/orm/SqlBinder.h>
 | 
					#include "drogon/orm/SqlBinder.h"
 | 
				
			||||||
#include <drogon/orm/Mapper.h>
 | 
					#include "drogon/orm/Mapper.h"
 | 
				
			||||||
#ifdef __cpp_impl_coroutine
 | 
					#ifdef __cpp_impl_coroutine
 | 
				
			||||||
#include <drogon/orm/CoroMapper.h>
 | 
					#include <drogon/orm/CoroMapper.h>
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
#include <trantor/utils/Date.h>
 | 
					#include "trantor/utils/Date.h"
 | 
				
			||||||
#include <trantor/utils/Logger.h>
 | 
					#include "trantor/utils/Logger.h"
 | 
				
			||||||
#include <json/json.h>
 | 
					#include "json/json.h"
 | 
				
			||||||
#include <string>
 | 
					#include <string>
 | 
				
			||||||
#include <memory>
 | 
					#include <memory>
 | 
				
			||||||
#include <vector>
 | 
					#include <vector>
 | 
				
			||||||
@@ -6,7 +6,7 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "User.h"
 | 
					#include "User.h"
 | 
				
			||||||
#include <drogon/utils/Utilities.h>
 | 
					#include "drogon/utils/Utilities.h"
 | 
				
			||||||
#include <string>
 | 
					#include <string>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using namespace drogon;
 | 
					using namespace drogon;
 | 
				
			||||||
@@ -6,17 +6,17 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pragma once
 | 
					#pragma once
 | 
				
			||||||
#include <drogon/orm/Result.h>
 | 
					#include "drogon/orm/Result.h"
 | 
				
			||||||
#include <drogon/orm/Row.h>
 | 
					#include "drogon/orm/Row.h"
 | 
				
			||||||
#include <drogon/orm/Field.h>
 | 
					#include "drogon/orm/Field.h"
 | 
				
			||||||
#include <drogon/orm/SqlBinder.h>
 | 
					#include "drogon/orm/SqlBinder.h"
 | 
				
			||||||
#include <drogon/orm/Mapper.h>
 | 
					#include "drogon/orm/Mapper.h"
 | 
				
			||||||
#ifdef __cpp_impl_coroutine
 | 
					#ifdef __cpp_impl_coroutine
 | 
				
			||||||
#include <drogon/orm/CoroMapper.h>
 | 
					#include <drogon/orm/CoroMapper.h>
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
#include <trantor/utils/Date.h>
 | 
					#include "trantor/utils/Date.h"
 | 
				
			||||||
#include <trantor/utils/Logger.h>
 | 
					#include "trantor/utils/Logger.h"
 | 
				
			||||||
#include <json/json.h>
 | 
					#include "json/json.h"
 | 
				
			||||||
#include <string>
 | 
					#include <string>
 | 
				
			||||||
#include <memory>
 | 
					#include <memory>
 | 
				
			||||||
#include <vector>
 | 
					#include <vector>
 | 
				
			||||||
							
								
								
									
										68
									
								
								backend/shl/msd/blocking_iterator.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								backend/shl/msd/blocking_iterator.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					// Copyright (C) 2022 Andrei Avram
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifndef MSD_CHANNEL_BLOCKING_ITERATOR_HPP_
 | 
				
			||||||
 | 
					#define MSD_CHANNEL_BLOCKING_ITERATOR_HPP_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <iterator>
 | 
				
			||||||
 | 
					#include <mutex>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace msd {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @brief An iterator that block the current thread,
 | 
				
			||||||
 | 
					 * waiting to fetch elements from the channel.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Used to implement channel range-based for loop.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @tparam Channel Instance of channel.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					template <typename channel>
 | 
				
			||||||
 | 
					class blocking_iterator {
 | 
				
			||||||
 | 
					   public:
 | 
				
			||||||
 | 
					    using value_type = typename channel::value_type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    explicit blocking_iterator(channel& ch) : ch_{ch} {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Advances to next element in the channel.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    blocking_iterator<channel> operator++() const noexcept { return *this; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns an element from the channel.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    value_type operator*() const
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        value_type value;
 | 
				
			||||||
 | 
					        value << ch_;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Makes iteration continue until the channel is closed and empty.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    bool operator!=(blocking_iterator<channel>) const
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        std::unique_lock<std::mutex> lock{ch_.mtx_};
 | 
				
			||||||
 | 
					        ch_.waitBeforeRead(lock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return !(ch_.closed() && ch_.empty());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   private:
 | 
				
			||||||
 | 
					    channel& ch_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace msd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @brief Output iterator specialization
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					struct std::iterator_traits<msd::blocking_iterator<T>> {
 | 
				
			||||||
 | 
					    using value_type = typename msd::blocking_iterator<T>::value_type;
 | 
				
			||||||
 | 
					    using iterator_category = std::output_iterator_tag;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#endif  // MSD_CHANNEL_BLOCKING_ITERATOR_HPP_
 | 
				
			||||||
							
								
								
									
										130
									
								
								backend/shl/msd/channel.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								backend/shl/msd/channel.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
				
			|||||||
 | 
					// Copyright (C) 2022 Andrei Avram
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifndef MSD_CHANNEL_HPP_
 | 
				
			||||||
 | 
					#define MSD_CHANNEL_HPP_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <atomic>
 | 
				
			||||||
 | 
					#include <condition_variable>
 | 
				
			||||||
 | 
					#include <cstdlib>
 | 
				
			||||||
 | 
					#include <mutex>
 | 
				
			||||||
 | 
					#include <queue>
 | 
				
			||||||
 | 
					#include <stdexcept>
 | 
				
			||||||
 | 
					#include <type_traits>
 | 
				
			||||||
 | 
					#include <utility>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "blocking_iterator.hpp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace msd {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#if (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L))
 | 
				
			||||||
 | 
					#define NODISCARD [[nodiscard]]
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
 | 
					#define NODISCARD
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace detail {
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					struct remove_cvref {
 | 
				
			||||||
 | 
					    using type = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					using remove_cvref_t = typename remove_cvref<T>::type;
 | 
				
			||||||
 | 
					}  // namespace detail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @brief Exception thrown if trying to write on closed channel.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class closed_channel : public std::runtime_error {
 | 
				
			||||||
 | 
					   public:
 | 
				
			||||||
 | 
					    explicit closed_channel(const char* msg) : std::runtime_error{msg} {}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @brief Thread-safe container for sharing data between threads.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Implements a blocking input iterator.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @tparam T The type of the elements.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					class channel {
 | 
				
			||||||
 | 
					   public:
 | 
				
			||||||
 | 
					    using value_type = T;
 | 
				
			||||||
 | 
					    using iterator = blocking_iterator<channel<T>>;
 | 
				
			||||||
 | 
					    using size_type = std::size_t;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Creates a new channel.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param capacity Number of elements the channel can store before blocking.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    explicit constexpr channel(size_type capacity = 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Pushes an element into the channel.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @throws closed_channel if channel is closed.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    template <typename Type>
 | 
				
			||||||
 | 
					    friend void operator>>(Type&&, channel<detail::remove_cvref_t<Type>>&);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Pops an element from the channel.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @tparam Type The type of the elements
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    template <typename Type>
 | 
				
			||||||
 | 
					    friend void operator<<(Type&, channel<Type>&);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns the number of elements in the channel.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    NODISCARD inline size_type constexpr size() const noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns true if there are no elements in channel.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    NODISCARD inline bool constexpr empty() const noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Closes the channel.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    inline void close() noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns true if the channel is closed.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    NODISCARD inline bool closed() const noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Iterator
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    iterator begin() noexcept;
 | 
				
			||||||
 | 
					    iterator end() noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Channel cannot be copied or moved.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    channel(const channel&) = delete;
 | 
				
			||||||
 | 
					    channel& operator=(const channel&) = delete;
 | 
				
			||||||
 | 
					    channel(channel&&) = delete;
 | 
				
			||||||
 | 
					    channel& operator=(channel&&) = delete;
 | 
				
			||||||
 | 
					    virtual ~channel() = default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   private:
 | 
				
			||||||
 | 
					    const size_type cap_;
 | 
				
			||||||
 | 
					    std::queue<T> queue_;
 | 
				
			||||||
 | 
					    std::mutex mtx_;
 | 
				
			||||||
 | 
					    std::condition_variable cnd_;
 | 
				
			||||||
 | 
					    std::atomic<bool> is_closed_{false};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    inline void waitBeforeRead(std::unique_lock<std::mutex>&);
 | 
				
			||||||
 | 
					    friend class blocking_iterator<channel>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "channel_impl.hpp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace msd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#endif  // MSD_CHANNEL_HPP_
 | 
				
			||||||
							
								
								
									
										87
									
								
								backend/shl/msd/channel_impl.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								backend/shl/msd/channel_impl.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					// Copyright (C) 2022 Andrei Avram
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					constexpr channel<T>::channel(const size_type capacity) : cap_{capacity}
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					void operator>>(T&& in, channel<detail::remove_cvref_t<T>>& ch)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    if (ch.closed()) {
 | 
				
			||||||
 | 
					        throw closed_channel{"cannot write on closed channel"};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    std::unique_lock<std::mutex> lock{ch.mtx_};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (ch.cap_ > 0 && ch.queue_.size() == ch.cap_) {
 | 
				
			||||||
 | 
					        ch.cnd_.wait(lock, [&ch]() { return ch.queue_.size() < ch.cap_; });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ch.queue_.push(std::forward<T>(in));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ch.cnd_.notify_one();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					void operator<<(T& out, channel<T>& ch)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    if (ch.closed() && ch.empty()) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        std::unique_lock<std::mutex> lock{ch.mtx_};
 | 
				
			||||||
 | 
					        ch.waitBeforeRead(lock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (ch.queue_.size() > 0) {
 | 
				
			||||||
 | 
					            out = std::move(ch.queue_.front());
 | 
				
			||||||
 | 
					            ch.queue_.pop();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ch.cnd_.notify_one();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					constexpr typename channel<T>::size_type channel<T>::size() const noexcept
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    return queue_.size();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					constexpr bool channel<T>::empty() const noexcept
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    return queue_.empty();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					void channel<T>::close() noexcept
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    is_closed_.store(true);
 | 
				
			||||||
 | 
					    cnd_.notify_all();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					bool channel<T>::closed() const noexcept
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    return is_closed_.load();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					blocking_iterator<channel<T>> channel<T>::begin() noexcept
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    return blocking_iterator<channel<T>>{*this};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					blocking_iterator<channel<T>> channel<T>::end() noexcept
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    return blocking_iterator<channel<T>>{*this};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template <typename T>
 | 
				
			||||||
 | 
					void channel<T>::waitBeforeRead(std::unique_lock<std::mutex>& lock)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    cnd_.wait(lock, [this] { return queue_.size() > 0 || closed(); });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -55,14 +55,15 @@ namespace api {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    void admin::delete_user(req_type req, cbk_type cbk) {
 | 
					    void admin::delete_user(req_type req, cbk_type cbk) {
 | 
				
			||||||
        Json::Value& json = *req->jsonObject();
 | 
					        Json::Value& json = *req->jsonObject();
 | 
				
			||||||
 | 
					        msd::channel<std::string> chan;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            uint64_t user_id = dto::json_get<uint64_t>(json, "user").value();
 | 
					            uint64_t user_id = dto::json_get<uint64_t>(json, "user").value();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            db::MapperUser user_mapper(drogon::app().getDbClient());
 | 
					            db::MapperUser user_mapper(drogon::app().getDbClient());
 | 
				
			||||||
            auto user = user_mapper.findByPrimaryKey(user_id);
 | 
					            auto user = user_mapper.findByPrimaryKey(user_id);
 | 
				
			||||||
            auth::revoke_all(user);
 | 
					            auth::revoke_all(user);
 | 
				
			||||||
             fs::delete_node(fs::get_node(user.getValueOfRootId()).value(), true);
 | 
					            fs::delete_node(fs::get_node(user.getValueOfRootId()).value(), chan, true);
 | 
				
			||||||
             user_mapper.deleteOne(user);
 | 
					            user_mapper.deleteOne(user);
 | 
				
			||||||
            cbk(dto::Responses::get_success_res());
 | 
					            cbk(dto::Responses::get_success_res());
 | 
				
			||||||
        } catch (const std::exception&) {
 | 
					        } catch (const std::exception&) {
 | 
				
			||||||
            cbk(dto::Responses::get_badreq_res("Validation error"));
 | 
					            cbk(dto::Responses::get_badreq_res("Validation error"));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@
 | 
				
			|||||||
#include <botan/base32.h>
 | 
					#include <botan/base32.h>
 | 
				
			||||||
#include <botan/base64.h>
 | 
					#include <botan/base64.h>
 | 
				
			||||||
#include <qrcodegen.hpp>
 | 
					#include <qrcodegen.hpp>
 | 
				
			||||||
#include <lodepng.h>
 | 
					#include <opencv2/opencv.hpp>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "controllers/controllers.h"
 | 
					#include "controllers/controllers.h"
 | 
				
			||||||
#include "db/db.h"
 | 
					#include "db/db.h"
 | 
				
			||||||
@@ -24,35 +24,14 @@ std::string create_totp_qrcode(const db::User& user, const std::string& b32_secr
 | 
				
			|||||||
    const int mod_count = code.getSize();
 | 
					    const int mod_count = code.getSize();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const int row_size = qrcode_pixel_size * mod_count;
 | 
					    const int row_size = qrcode_pixel_size * mod_count;
 | 
				
			||||||
    std::vector<uint8_t> secret, image, row;
 | 
					    cv::Mat image(mod_count, mod_count, CV_8UC1), scaled_image;
 | 
				
			||||||
    row.reserve(row_size);
 | 
					    std::vector<uint8_t> image_encoded;
 | 
				
			||||||
    image.reserve(row_size * row_size);
 | 
					    for (int y = 0; y < mod_count; y++) for (int x = 0; x < mod_count; x++)
 | 
				
			||||||
 | 
					        image.at<uint8_t>(x, y) = code.getModule(x, y) ? 0 : 0xff;
 | 
				
			||||||
 | 
					    cv::resize(image, scaled_image, cv::Size(), qrcode_pixel_size, qrcode_pixel_size, cv::INTER_NEAREST);
 | 
				
			||||||
 | 
					    cv::imencode(".png", scaled_image, image_encoded);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (int y = 0; y < mod_count; y++) {
 | 
					    return "data:image/png;base64," + Botan::base64_encode(image_encoded);
 | 
				
			||||||
        row.clear();
 | 
					 | 
				
			||||||
        for (int x = 0; x < mod_count; x++)
 | 
					 | 
				
			||||||
            row.insert(row.end(), qrcode_pixel_size, code.getModule(x, y) ? 0 : 0xff);
 | 
					 | 
				
			||||||
        for (int i = 0; i < qrcode_pixel_size; i++)
 | 
					 | 
				
			||||||
            image.insert(image.end(), row.begin(), row.end());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    lodepng::encode(secret, image, row_size, row_size, LCT_GREY, 8);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /*
 | 
					 | 
				
			||||||
    png::image<png::gray_pixel> image(mod_count*qrcode_pixel_size, mod_count*qrcode_pixel_size);
 | 
					 | 
				
			||||||
    for (int x = 0; x < mod_count; x++) for (int y = 0; y < mod_count; y++) {
 | 
					 | 
				
			||||||
        const bool mod = code.getModule(x, y);
 | 
					 | 
				
			||||||
        const int x_img_start = x * qrcode_pixel_size, y_img_start = y * qrcode_pixel_size;
 | 
					 | 
				
			||||||
        for (int x_img = x_img_start; x_img < x_img_start + qrcode_pixel_size; x_img++) for (int y_img = y_img_start; y_img < y_img_start + qrcode_pixel_size; y_img++)
 | 
					 | 
				
			||||||
            image[x_img][y_img] = mod ? 0 : 0xff;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    std::stringstream image_ss;
 | 
					 | 
				
			||||||
    image.write_stream(image_ss);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    std::string image_str = image_ss.str();
 | 
					 | 
				
			||||||
    std::vector<uint8_t> secret(image_str.data(), image_str.data()+image_str.size());
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
    return "data:image/png;base64," + Botan::base64_encode(secret);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace api {
 | 
					namespace api {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,16 +17,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
 | 
					#include <jwt-cpp/traits/kazuho-picojson/traits.h>
 | 
				
			||||||
#include <jwt-cpp/jwt.h>
 | 
					#include <jwt-cpp/jwt.h>
 | 
				
			||||||
#include <curl/curl.h>
 | 
					#include <SMTPMail.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "controllers/controllers.h"
 | 
					#include "controllers/controllers.h"
 | 
				
			||||||
#include "db/db.h"
 | 
					#include "db/db.h"
 | 
				
			||||||
#include "dto/dto.h"
 | 
					#include "dto/dto.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
size_t payload_source(char* ptr, size_t size, size_t nmemb, void* userp) {
 | 
					 | 
				
			||||||
    auto* ss = (std::stringstream*)userp;
 | 
					 | 
				
			||||||
    return ss->readsome(ptr, (long)(size*nmemb));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace api {
 | 
					namespace api {
 | 
				
			||||||
#if defined(BOTAN_HAS_SYSTEM_RNG)
 | 
					#if defined(BOTAN_HAS_SYSTEM_RNG)
 | 
				
			||||||
@@ -42,33 +38,22 @@ namespace api {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void auth::send_mail(const db::User& user) {
 | 
					    void auth::send_mail(const db::User& user) {
 | 
				
			||||||
        std::stringstream ss;
 | 
					 | 
				
			||||||
        std::time_t t = std::time(nullptr);
 | 
					        std::time_t t = std::time(nullptr);
 | 
				
			||||||
        const auto& totp_secret = (const std::vector<uint8_t>&) user.getValueOfTfaSecret();
 | 
					        const auto& totp_secret = (const std::vector<uint8_t>&) user.getValueOfTfaSecret();
 | 
				
			||||||
        char totp[16];
 | 
					        char totp[16];
 | 
				
			||||||
        std::snprintf(totp, 16, "%06d", Botan::TOTP(Botan::OctetString(totp_secret)).generate_totp(t));
 | 
					        std::snprintf(totp, 16, "%06d", Botan::TOTP(Botan::OctetString(totp_secret)).generate_totp(t));
 | 
				
			||||||
        ss.imbue(std::locale("en_US.utf8"));
 | 
					 | 
				
			||||||
        ss << "Date: " << std::put_time(std::localtime(&t), "%a, %d %b %Y %T %z") << "\r\n";
 | 
					 | 
				
			||||||
        ss << "To: " << user.getValueOfName() << "\r\n";
 | 
					 | 
				
			||||||
        ss << "From: fileserver@mattv.de\r\n";
 | 
					 | 
				
			||||||
        ss << "Message-ID: " << Botan::UUID(*rng).to_string() << "@mattv.de>\r\n";
 | 
					 | 
				
			||||||
        ss << "Subject: Fileserver - EMail 2fa code\r\n";
 | 
					 | 
				
			||||||
        ss << "Your code is: " << totp << "\r\n";
 | 
					 | 
				
			||||||
        ss << "It is valid for 5 Minutes\r\n";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        CURL* curl = curl_easy_init();
 | 
					        drogon::app().getPlugin<SMTPMail>()->sendEmail(
 | 
				
			||||||
        curl_easy_setopt(curl, CURLOPT_USERNAME, "no-reply@mattv.de");
 | 
					                "mail.mattv.de",
 | 
				
			||||||
        curl_easy_setopt(curl, CURLOPT_PASSWORD, "noreplyLONGPASS123");
 | 
					                587,
 | 
				
			||||||
        curl_easy_setopt(curl, CURLOPT_URL, "smtp://mail.mattv.de:587");
 | 
					                "fileserver@mattv.de",
 | 
				
			||||||
        curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
 | 
					                user.getValueOfName(),
 | 
				
			||||||
        auto recp = curl_slist_append(nullptr, user.getValueOfName().c_str());
 | 
					                "MFileserver - Email 2fa code",
 | 
				
			||||||
        curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recp);
 | 
					                "Your code is: " + std::string(totp) +"\r\nIt is valid for 5 Minutes",
 | 
				
			||||||
        curl_easy_setopt(curl, CURLOPT_READFUNCTION, &payload_source);
 | 
					                "no-reply@mattv.de",
 | 
				
			||||||
        curl_easy_setopt(curl, CURLOPT_READDATA, &ss);
 | 
					                "noreplyLONGPASS123",
 | 
				
			||||||
        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
 | 
					                false
 | 
				
			||||||
        curl_easy_perform(curl);
 | 
					        );
 | 
				
			||||||
        curl_slist_free_all(recp);
 | 
					 | 
				
			||||||
        curl_easy_cleanup(curl);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    std::string auth::get_token(const db::User& user) {
 | 
					    std::string auth::get_token(const db::User& user) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,11 @@
 | 
				
			|||||||
#ifndef BACKEND_CONTROLLERS_H
 | 
					#ifndef BACKEND_CONTROLLERS_H
 | 
				
			||||||
#define BACKEND_CONTROLLERS_H
 | 
					#define BACKEND_CONTROLLERS_H
 | 
				
			||||||
#include <drogon/drogon.h>
 | 
					 | 
				
			||||||
#include <drogon/utils/coroutine.h>
 | 
					 | 
				
			||||||
#include <botan/rng.h>
 | 
					 | 
				
			||||||
#include <coroutine>
 | 
					 | 
				
			||||||
#include <variant>
 | 
					#include <variant>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <drogon/drogon.h>
 | 
				
			||||||
 | 
					#include <botan/rng.h>
 | 
				
			||||||
 | 
					#include <msd/channel.hpp>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "db/db.h"
 | 
					#include "db/db.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using req_type = const drogon::HttpRequestPtr&;
 | 
					using req_type = const drogon::HttpRequestPtr&;
 | 
				
			||||||
@@ -86,14 +86,31 @@ public:
 | 
				
			|||||||
        METHOD_ADD(fs::create_node_req<true>, "/createFile", drogon::Post, "Login");
 | 
					        METHOD_ADD(fs::create_node_req<true>, "/createFile", drogon::Post, "Login");
 | 
				
			||||||
        METHOD_ADD(fs::delete_node_req, "/delete/{}", drogon::Post, "Login");
 | 
					        METHOD_ADD(fs::delete_node_req, "/delete/{}", drogon::Post, "Login");
 | 
				
			||||||
        METHOD_ADD(fs::upload, "/upload/{}", drogon::Post, "Login");
 | 
					        METHOD_ADD(fs::upload, "/upload/{}", drogon::Post, "Login");
 | 
				
			||||||
 | 
					        METHOD_ADD(fs::create_zip, "/create_zip", drogon::Post, "Login");
 | 
				
			||||||
        METHOD_ADD(fs::download, "/download", drogon::Post, "Login");
 | 
					        METHOD_ADD(fs::download, "/download", drogon::Post, "Login");
 | 
				
			||||||
 | 
					        METHOD_ADD(fs::download_multi, "/download_multi", drogon::Post, "Login");
 | 
				
			||||||
 | 
					        METHOD_ADD(fs::download_preview, "/download_preview/{}", drogon::Get, "Login");
 | 
				
			||||||
 | 
					        METHOD_ADD(fs::get_type, "/get_type/{}", drogon::Get, "Login");
 | 
				
			||||||
    METHOD_LIST_END
 | 
					    METHOD_LIST_END
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    enum class create_node_error {
 | 
				
			||||||
 | 
					        INVALID_NAME,
 | 
				
			||||||
 | 
					        INVALID_PARENT,
 | 
				
			||||||
 | 
					        FILE_PARENT
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    struct mutex_stream {
 | 
				
			||||||
 | 
					        std::stringstream ss;
 | 
				
			||||||
 | 
					        std::mutex mutex;
 | 
				
			||||||
 | 
					        bool done = false;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static std::optional<db::INode> get_node(uint64_t node);
 | 
					    static std::optional<db::INode> get_node(uint64_t node);
 | 
				
			||||||
    static std::optional<db::INode> get_node_and_validate(const db::User& user, uint64_t node);
 | 
					    static std::optional<db::INode> get_node_and_validate(const db::User& user, uint64_t node);
 | 
				
			||||||
    static std::vector<db::INode> get_children(const db::INode& parent);
 | 
					    static std::vector<db::INode> get_children(const db::INode& parent);
 | 
				
			||||||
    static std::variant<db::INode, std::string> create_node(std::string name, const db::User& owner, bool file, const std::optional<uint64_t> &parent, bool force = false);
 | 
					    static std::variant<db::INode, fs::create_node_error, std::tuple<bool, uint64_t>>
 | 
				
			||||||
    static void delete_node(db::INode node, bool allow_root = false);
 | 
					        create_node(std::string name, const db::User& owner, bool file, const std::optional<uint64_t> &parent, bool force = false);
 | 
				
			||||||
 | 
					    static void delete_node(db::INode node, msd::channel<std::string>& chan, bool allow_root = false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void root(req_type, cbk_type);
 | 
					    void root(req_type, cbk_type);
 | 
				
			||||||
@@ -102,7 +119,11 @@ public:
 | 
				
			|||||||
    template<bool file> void create_node_req(req_type req, cbk_type cbk);
 | 
					    template<bool file> void create_node_req(req_type req, cbk_type cbk);
 | 
				
			||||||
    void delete_node_req(req_type, cbk_type, uint64_t node);
 | 
					    void delete_node_req(req_type, cbk_type, uint64_t node);
 | 
				
			||||||
    void upload(req_type, cbk_type, uint64_t node);
 | 
					    void upload(req_type, cbk_type, uint64_t node);
 | 
				
			||||||
 | 
					    void create_zip(req_type, cbk_type);
 | 
				
			||||||
    void download(req_type, cbk_type);
 | 
					    void download(req_type, cbk_type);
 | 
				
			||||||
 | 
					    void download_multi(req_type, cbk_type);
 | 
				
			||||||
 | 
					    void download_preview(req_type, cbk_type, uint64_t node);
 | 
				
			||||||
 | 
					    void get_type(req_type, cbk_type, uint64_t node);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class user : public drogon::HttpController<user> {
 | 
					class user : public drogon::HttpController<user> {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,12 +3,100 @@
 | 
				
			|||||||
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
 | 
					#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <filesystem>
 | 
					#include <filesystem>
 | 
				
			||||||
 | 
					#include <unordered_map>
 | 
				
			||||||
 | 
					#include <fstream>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <opencv2/opencv.hpp>
 | 
				
			||||||
 | 
					#include <botan/base64.h>
 | 
				
			||||||
 | 
					#include <trantor/net/EventLoopThread.h>
 | 
				
			||||||
 | 
					#include <zip/zip.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "controllers.h"
 | 
					#include "controllers.h"
 | 
				
			||||||
#include "dto/dto.h"
 | 
					#include "dto/dto.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
char windows_invalid_chars[] = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F<>:\"/\\|";
 | 
					char windows_invalid_chars[] = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F<>:\"/\\|";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
std::string generate_path(db::INode node) {
 | 
					// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#common_image_file_types
 | 
				
			||||||
 | 
					const std::unordered_map<std::string, std::string> mime_type_map = {
 | 
				
			||||||
 | 
					        { ".apng" , "image/apng" },
 | 
				
			||||||
 | 
					        { ".avif" , "image/avif" },
 | 
				
			||||||
 | 
					        { ".bmp"  , "image/bmp"  },
 | 
				
			||||||
 | 
					        { ".gif"  , "image/gif"  },
 | 
				
			||||||
 | 
					        { ".jpg"  , "image/jpeg" },
 | 
				
			||||||
 | 
					        { ".jpeg" , "image/jpeg" },
 | 
				
			||||||
 | 
					        { ".jfif" , "image/jpeg" },
 | 
				
			||||||
 | 
					        { ".pjpeg", "image/jpeg" },
 | 
				
			||||||
 | 
					        { ".pjp"  , "image/jpeg" },
 | 
				
			||||||
 | 
					        { ".png"  , "image/png"  },
 | 
				
			||||||
 | 
					        { ".svg"  , "image/svg"  },
 | 
				
			||||||
 | 
					        { ".webp" , "image/webp" },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        { ".aac"  , "audio/aac"  },
 | 
				
			||||||
 | 
					        { ".flac" , "audio/flac" },
 | 
				
			||||||
 | 
					        { ".mp3"  , "audio/mp3"  },
 | 
				
			||||||
 | 
					        { ".m4a"  , "audio/mp4"  },
 | 
				
			||||||
 | 
					        { ".oga"  , "audio/ogg"  },
 | 
				
			||||||
 | 
					        { ".ogg"  , "audio/ogg"  },
 | 
				
			||||||
 | 
					        { ".wav"  , "audio/wav"  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        { ".3gp"  , "video/3gpp" },
 | 
				
			||||||
 | 
					        { ".mpg"  , "video/mpeg" },
 | 
				
			||||||
 | 
					        { ".mpeg" , "video/mpeg" },
 | 
				
			||||||
 | 
					        { ".mp4"  , "video/mp4"  },
 | 
				
			||||||
 | 
					        { ".m4v"  , "video/mp4"  },
 | 
				
			||||||
 | 
					        { ".m4p"  , "video/mp4"  },
 | 
				
			||||||
 | 
					        { ".ogv"  , "video/ogg"  },
 | 
				
			||||||
 | 
					        { ".mov"  , "video/quicktime" },
 | 
				
			||||||
 | 
					        { ".webm" , "video/webm" },
 | 
				
			||||||
 | 
					        { ".mkv"  , "video/x-matroska" },
 | 
				
			||||||
 | 
					        { ".mk3d" , "video/x-matroska" },
 | 
				
			||||||
 | 
					        { ".mks"  , "video/x-matroska" },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint64_t next_temp_id = 0;
 | 
				
			||||||
 | 
					std::unordered_map<std::string, std::string> zip_to_temp_map;
 | 
				
			||||||
 | 
					std::unordered_map<std::string, std::tuple<std::string, uint64_t, uint64_t>> in_progress_zips;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trantor::EventLoop* get_zip_loop() {
 | 
				
			||||||
 | 
					    static bool init_done = false;
 | 
				
			||||||
 | 
					    static trantor::EventLoopThread loop("ZipEventLoop");
 | 
				
			||||||
 | 
					    if (!init_done) {
 | 
				
			||||||
 | 
					        init_done = true;
 | 
				
			||||||
 | 
					        loop.run();
 | 
				
			||||||
 | 
					        loop.getLoop()->runEvery(30*60, []{
 | 
				
			||||||
 | 
					            for (const auto& entry : std::filesystem::directory_iterator("./temp")) {
 | 
				
			||||||
 | 
					                if (!entry.is_regular_file()) continue;
 | 
				
			||||||
 | 
					                const std::string file_name = "./temp/" + entry.path().filename().string();
 | 
				
			||||||
 | 
					                const auto& progress_pos = std::find_if(in_progress_zips.begin(), in_progress_zips.end(),
 | 
				
			||||||
 | 
					                    [&file_name](const std::pair<std::string, std::tuple<std::string, uint64_t, uint64_t>>& entry) {
 | 
				
			||||||
 | 
					                        return std::get<0>(entry.second) == file_name;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                if (progress_pos != in_progress_zips.end()) return;
 | 
				
			||||||
 | 
					                const auto& zip_map_pos = std::find_if(zip_to_temp_map.begin(), zip_to_temp_map.end(),
 | 
				
			||||||
 | 
					                    [&file_name](const std::pair<std::string, std::string>& entry){
 | 
				
			||||||
 | 
					                        return entry.second == file_name;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                if (zip_map_pos != zip_to_temp_map.end()) return;
 | 
				
			||||||
 | 
					                std::filesystem::remove(entry.path());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return loop.getLoop();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trantor::EventLoop* get_delete_loop() {
 | 
				
			||||||
 | 
					    static bool init_done = false;
 | 
				
			||||||
 | 
					    static trantor::EventLoopThread loop("DeleteEventLoop");
 | 
				
			||||||
 | 
					    if (!init_done) {
 | 
				
			||||||
 | 
					        init_done = true;
 | 
				
			||||||
 | 
					        loop.run();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return loop.getLoop();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void generate_path(db::INode node, std::string& str) {
 | 
				
			||||||
    db::MapperInode inode_mapper(drogon::app().getDbClient());
 | 
					    db::MapperInode inode_mapper(drogon::app().getDbClient());
 | 
				
			||||||
    std::stack<db::INode> path;
 | 
					    std::stack<db::INode> path;
 | 
				
			||||||
    path.push(node);
 | 
					    path.push(node);
 | 
				
			||||||
@@ -16,14 +104,101 @@ std::string generate_path(db::INode node) {
 | 
				
			|||||||
        node = inode_mapper.findByPrimaryKey(node.getValueOfParentId());
 | 
					        node = inode_mapper.findByPrimaryKey(node.getValueOfParentId());
 | 
				
			||||||
        path.push(node);
 | 
					        path.push(node);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    std::stringstream ss;
 | 
					 | 
				
			||||||
    while (!path.empty()) {
 | 
					    while (!path.empty()) {
 | 
				
			||||||
        const db::INode& seg = path.top();
 | 
					        const db::INode& seg = path.top();
 | 
				
			||||||
        ss << seg.getValueOfName();
 | 
					        str += seg.getValueOfName();
 | 
				
			||||||
        if (seg.getValueOfIsFile() == 0) ss << '/';
 | 
					        if (seg.getValueOfIsFile() == 0) str += "/";
 | 
				
			||||||
        path.pop();
 | 
					        path.pop();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return ss.str();
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Json::Value generate_path(db::INode node) {
 | 
				
			||||||
 | 
					    Json::Value segments = Json::Value(Json::ValueType::arrayValue);
 | 
				
			||||||
 | 
					    db::MapperInode inode_mapper(drogon::app().getDbClient());
 | 
				
			||||||
 | 
					    std::stack<db::INode> path;
 | 
				
			||||||
 | 
					    path.push(node);
 | 
				
			||||||
 | 
					    while (node.getParentId() != nullptr) {
 | 
				
			||||||
 | 
					        node = inode_mapper.findByPrimaryKey(node.getValueOfParentId());
 | 
				
			||||||
 | 
					        path.push(node);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    while (!path.empty()) {
 | 
				
			||||||
 | 
					        const db::INode& seg = path.top();
 | 
				
			||||||
 | 
					        if (seg.getParentId() == nullptr) {
 | 
				
			||||||
 | 
					            Json::Value json_seg;
 | 
				
			||||||
 | 
					            json_seg["path"] = "/";
 | 
				
			||||||
 | 
					            json_seg["node"] = seg.getValueOfId();
 | 
				
			||||||
 | 
					            segments.append(json_seg);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Json::Value json_seg;
 | 
				
			||||||
 | 
					            json_seg["path"] = seg.getValueOfName();
 | 
				
			||||||
 | 
					            json_seg["node"] = seg.getValueOfId();
 | 
				
			||||||
 | 
					            segments.append(json_seg);
 | 
				
			||||||
 | 
					            if (seg.getValueOfIsFile() == 0) {
 | 
				
			||||||
 | 
					                json_seg.removeMember("node");
 | 
				
			||||||
 | 
					                json_seg["path"] = "/";
 | 
				
			||||||
 | 
					                segments.append(json_seg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        path.pop();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Json::Value resp;
 | 
				
			||||||
 | 
					    resp["segments"] = segments;
 | 
				
			||||||
 | 
					    return resp;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint64_t calc_total_size(const db::INode& base) {
 | 
				
			||||||
 | 
					    uint64_t size = 0;
 | 
				
			||||||
 | 
					    std::stack<db::INode> queue;
 | 
				
			||||||
 | 
					    queue.push(base);
 | 
				
			||||||
 | 
					    while (!queue.empty()) {
 | 
				
			||||||
 | 
					        const db::INode& node = queue.top();
 | 
				
			||||||
 | 
					        if (node.getValueOfIsFile() == 0) {
 | 
				
			||||||
 | 
					            auto children = api::fs::get_children(node);
 | 
				
			||||||
 | 
					            queue.pop();
 | 
				
			||||||
 | 
					            for (const auto& child : children) {
 | 
				
			||||||
 | 
					                if (child.getValueOfIsFile() == 0) queue.push(child);
 | 
				
			||||||
 | 
					                else if (child.getSize()) size += child.getValueOfSize();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            size += node.getValueOfSize();
 | 
				
			||||||
 | 
					            queue.pop();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return size;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void add_to_zip(struct zip_t* zip, const std::string& key, const db::INode& node, const std::string& path) {
 | 
				
			||||||
 | 
					    if (node.getValueOfIsFile() == 0) {
 | 
				
			||||||
 | 
					        std::string new_path = path + node.getValueOfName() + "/";
 | 
				
			||||||
 | 
					        zip_entry_opencasesensitive(zip, new_path.c_str());
 | 
				
			||||||
 | 
					        zip_entry_close(zip);
 | 
				
			||||||
 | 
					        auto children = api::fs::get_children(node);
 | 
				
			||||||
 | 
					        for (const auto& child : children)
 | 
				
			||||||
 | 
					            add_to_zip(zip, key, child, new_path);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        zip_entry_opencasesensitive(zip, (path + node.getValueOfName()).c_str());
 | 
				
			||||||
 | 
					        std::ifstream file("./files/" + std::to_string(node.getValueOfId()), std::ifstream::binary);
 | 
				
			||||||
 | 
					        std::vector<char> buffer(64*1024);
 | 
				
			||||||
 | 
					        while (!file.eof()) {
 | 
				
			||||||
 | 
					            file.read(buffer.data(), (std::streamsize)buffer.size());
 | 
				
			||||||
 | 
					            auto read = file.gcount();
 | 
				
			||||||
 | 
					            zip_entry_write(zip, buffer.data(), read);
 | 
				
			||||||
 | 
					            std::get<1>(in_progress_zips[key]) += read;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        zip_entry_close(zip);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template<typename InputIt>
 | 
				
			||||||
 | 
					std::string join_string(InputIt first, InputIt last, const std::string& separator = ",") {
 | 
				
			||||||
 | 
					    std::ostringstream result;
 | 
				
			||||||
 | 
					    if (first != last) {
 | 
				
			||||||
 | 
					        result << *first;
 | 
				
			||||||
 | 
					        while (++first != last) {
 | 
				
			||||||
 | 
					            result << separator << *first;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.str();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace api {
 | 
					namespace api {
 | 
				
			||||||
@@ -48,26 +223,31 @@ namespace api {
 | 
				
			|||||||
        return inode_mapper.findBy(db::Criteria(db::INode::Cols::_parent_id, db::CompareOps::EQ, parent.getValueOfId()));
 | 
					        return inode_mapper.findBy(db::Criteria(db::INode::Cols::_parent_id, db::CompareOps::EQ, parent.getValueOfId()));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    std::variant<db::INode, std::string> fs::create_node(std::string name, const db::User& owner, bool file, const std::optional<uint64_t> &parent, bool force) {
 | 
					    std::variant<db::INode, fs::create_node_error, std::tuple<bool, uint64_t>>
 | 
				
			||||||
 | 
					    fs::create_node(std::string name, const db::User& owner, bool file, const std::optional<uint64_t> &parent, bool force) {
 | 
				
			||||||
        // Stolen from https://github.com/boostorg/filesystem/blob/develop/src/portability.cpp
 | 
					        // Stolen from https://github.com/boostorg/filesystem/blob/develop/src/portability.cpp
 | 
				
			||||||
        if (!force)
 | 
					        if (!force)
 | 
				
			||||||
            if (name.empty() || name[0] == ' ' || name.find_first_of(windows_invalid_chars, 0, sizeof(windows_invalid_chars)) != std::string::npos || *(name.end() - 1) == ' ' || *(name.end() - 1) == '.' || name == "." || name == "..")
 | 
					            if (name.empty() || name[0] == ' ' || name.find_first_of(windows_invalid_chars, 0, sizeof(windows_invalid_chars)) != std::string::npos || *(name.end() - 1) == ' ' || *(name.end() - 1) == '.' || name == "." || name == "..")
 | 
				
			||||||
                return {"Invalid name"};
 | 
					                return {create_node_error::INVALID_NAME};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        db::INode node;
 | 
					        db::INode node;
 | 
				
			||||||
        node.setIsFile(file ? 1 : 0);
 | 
					        node.setIsFile(file ? 1 : 0);
 | 
				
			||||||
        node.setName(name);
 | 
					        node.setName(name);
 | 
				
			||||||
        node.setOwnerId(owner.getValueOfId());
 | 
					        node.setOwnerId(owner.getValueOfId());
 | 
				
			||||||
 | 
					        node.setHasPreview(0);
 | 
				
			||||||
        if (parent.has_value()) {
 | 
					        if (parent.has_value()) {
 | 
				
			||||||
            auto parent_node =  get_node_and_validate(owner, *parent);
 | 
					            auto parent_node =  get_node_and_validate(owner, *parent);
 | 
				
			||||||
            if (!parent_node.has_value())
 | 
					            if (!parent_node.has_value())
 | 
				
			||||||
                return {"Invalid parent"};
 | 
					                return {create_node_error::INVALID_PARENT};
 | 
				
			||||||
            if (parent_node->getValueOfIsFile() != 0)
 | 
					            if (parent_node->getValueOfIsFile() != 0)
 | 
				
			||||||
                return {"Can't use file as parent"};
 | 
					                return {create_node_error::FILE_PARENT};
 | 
				
			||||||
            auto children = get_children(*parent_node);
 | 
					            auto children = get_children(*parent_node);
 | 
				
			||||||
            for (const auto& child : children)
 | 
					            for (const auto& child : children)
 | 
				
			||||||
                if (child.getValueOfName() == name)
 | 
					                if (child.getValueOfName() == name)
 | 
				
			||||||
                    return {"File/Folder already exists"};
 | 
					                    return {std::make_tuple(
 | 
				
			||||||
 | 
					                                child.getValueOfIsFile() != 0,
 | 
				
			||||||
 | 
					                                child.getValueOfId()
 | 
				
			||||||
 | 
					                            )};
 | 
				
			||||||
            node.setParentId(*parent);
 | 
					            node.setParentId(*parent);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        db::MapperInode inode_mapper(drogon::app().getDbClient());
 | 
					        db::MapperInode inode_mapper(drogon::app().getDbClient());
 | 
				
			||||||
@@ -75,18 +255,56 @@ namespace api {
 | 
				
			|||||||
        return {node};
 | 
					        return {node};
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void fs::delete_node(db::INode node, bool allow_root) {
 | 
					    void fs::delete_node(db::INode node, msd::channel<std::string>& chan, bool allow_root) {
 | 
				
			||||||
        if (node.getValueOfParentId() == 0 && (!allow_root)) return;
 | 
					        if (node.getValueOfParentId() == 0 && (!allow_root)) return;
 | 
				
			||||||
        if (node.getValueOfIsFile() == 0) {
 | 
					
 | 
				
			||||||
            auto children =  get_children(node);
 | 
					        db::MapperInode inode_mapper(drogon::app().getDbClient());
 | 
				
			||||||
            for (const auto& child : children) delete_node(child, false);
 | 
					
 | 
				
			||||||
        } else {
 | 
					        const auto delete_file = [&chan, &inode_mapper](const db::INode& node) {
 | 
				
			||||||
 | 
					            std::string entry = "Deleting ";
 | 
				
			||||||
 | 
					            generate_path(node, entry);
 | 
				
			||||||
 | 
					            entry >> chan;
 | 
				
			||||||
            std::filesystem::path p("./files");
 | 
					            std::filesystem::path p("./files");
 | 
				
			||||||
            p /= std::to_string(node.getValueOfId());
 | 
					            p /= std::to_string(node.getValueOfId());
 | 
				
			||||||
            std::filesystem::remove(p);
 | 
					            std::filesystem::remove(p);
 | 
				
			||||||
 | 
					            if (node.getValueOfHasPreview() != 0)
 | 
				
			||||||
 | 
					                std::filesystem::remove(p.string() + "_preview.png");
 | 
				
			||||||
 | 
					            inode_mapper.deleteOne(node);
 | 
				
			||||||
 | 
					            std::string(" Done\n") >> chan;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        std::stack<db::INode> queue, files, folders;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (node.getValueOfIsFile() == 0) queue.push(node);
 | 
				
			||||||
 | 
					        else files.push(node);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while (!queue.empty()) {
 | 
				
			||||||
 | 
					            while (!files.empty()) {
 | 
				
			||||||
 | 
					                delete_file(files.top());
 | 
				
			||||||
 | 
					                files.pop();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            std::string entry = "Deleting ";
 | 
				
			||||||
 | 
					            generate_path(queue.top(), entry);
 | 
				
			||||||
 | 
					            entry += "\n";
 | 
				
			||||||
 | 
					            entry >> chan;
 | 
				
			||||||
 | 
					            auto children = get_children(queue.top());
 | 
				
			||||||
 | 
					            folders.push(queue.top());
 | 
				
			||||||
 | 
					            queue.pop();
 | 
				
			||||||
 | 
					            for (const auto& child : children) {
 | 
				
			||||||
 | 
					                if (child.getValueOfIsFile() == 0) queue.push(child);
 | 
				
			||||||
 | 
					                else files.push(child);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while (!files.empty()) {
 | 
				
			||||||
 | 
					            delete_file(files.top());
 | 
				
			||||||
 | 
					            files.pop();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while (!folders.empty()) {
 | 
				
			||||||
 | 
					            inode_mapper.deleteOne(folders.top());
 | 
				
			||||||
 | 
					            folders.pop();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        db::MapperInode inode_mapper(drogon::app().getDbClient());
 | 
					 | 
				
			||||||
        inode_mapper.deleteOne(node);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void fs::root(req_type req, cbk_type cbk) {
 | 
					    void fs::root(req_type req, cbk_type cbk) {
 | 
				
			||||||
@@ -98,23 +316,11 @@ namespace api {
 | 
				
			|||||||
        db::User user = dto::get_user(req);
 | 
					        db::User user = dto::get_user(req);
 | 
				
			||||||
        auto inode =  get_node_and_validate(user, node);
 | 
					        auto inode =  get_node_and_validate(user, node);
 | 
				
			||||||
        if (!inode.has_value())
 | 
					        if (!inode.has_value())
 | 
				
			||||||
            cbk(dto::Responses::get_badreq_res("Unknown node"));
 | 
					            return cbk(dto::Responses::get_badreq_res("Unknown node"));
 | 
				
			||||||
        else if (inode->getValueOfIsFile() == 0) {
 | 
					        auto dto_node = dto::Responses::GetNodeEntry(*inode);
 | 
				
			||||||
            std::vector<uint64_t> children;
 | 
					        std::vector<dto::Responses::GetNodeEntry> children;
 | 
				
			||||||
            for (const db::INode& child : get_children(*inode)) children.push_back(child.getValueOfId());
 | 
					        if (!dto_node.is_file) for (const db::INode& child : get_children(*inode)) children.emplace_back(child);
 | 
				
			||||||
            cbk(dto::Responses::get_node_folder_res(
 | 
					        cbk(dto::Responses::get_node_res(dto_node, children));
 | 
				
			||||||
                    inode->getValueOfId(),
 | 
					 | 
				
			||||||
                    inode->getValueOfName(),
 | 
					 | 
				
			||||||
                    inode->getParentId(),
 | 
					 | 
				
			||||||
                    children
 | 
					 | 
				
			||||||
            ));
 | 
					 | 
				
			||||||
        } else
 | 
					 | 
				
			||||||
            cbk(dto::Responses::get_node_file_res(
 | 
					 | 
				
			||||||
                    inode->getValueOfId(),
 | 
					 | 
				
			||||||
                    inode->getValueOfName(),
 | 
					 | 
				
			||||||
                    inode->getParentId(),
 | 
					 | 
				
			||||||
                    inode->getValueOfSize()
 | 
					 | 
				
			||||||
            ));
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void fs::path(req_type req, cbk_type cbk, uint64_t node) {
 | 
					    void fs::path(req_type req, cbk_type cbk, uint64_t node) {
 | 
				
			||||||
@@ -122,8 +328,10 @@ namespace api {
 | 
				
			|||||||
        auto inode = get_node_and_validate(user, node);
 | 
					        auto inode = get_node_and_validate(user, node);
 | 
				
			||||||
        if (!inode.has_value())
 | 
					        if (!inode.has_value())
 | 
				
			||||||
            cbk(dto::Responses::get_badreq_res("Unknown node"));
 | 
					            cbk(dto::Responses::get_badreq_res("Unknown node"));
 | 
				
			||||||
        else
 | 
					        else {
 | 
				
			||||||
            cbk(dto::Responses::get_path_res( generate_path(*inode)));
 | 
					            auto path = generate_path(*inode);
 | 
				
			||||||
 | 
					            cbk(dto::Responses::get_success_res(path));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template<bool file>
 | 
					    template<bool file>
 | 
				
			||||||
@@ -135,10 +343,18 @@ namespace api {
 | 
				
			|||||||
            std::string name = dto::json_get<std::string>(json, "name").value();
 | 
					            std::string name = dto::json_get<std::string>(json, "name").value();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            auto new_node = create_node(name, user, file, std::make_optional(parent));
 | 
					            auto new_node = create_node(name, user, file, std::make_optional(parent));
 | 
				
			||||||
            if (std::holds_alternative<std::string>(new_node))
 | 
					            if (std::holds_alternative<db::INode>(new_node))
 | 
				
			||||||
                cbk(dto::Responses::get_badreq_res(std::get<std::string>(new_node)));
 | 
					 | 
				
			||||||
            else
 | 
					 | 
				
			||||||
                cbk(dto::Responses::get_new_node_res(std::get<db::INode>(new_node).getValueOfId()));
 | 
					                cbk(dto::Responses::get_new_node_res(std::get<db::INode>(new_node).getValueOfId()));
 | 
				
			||||||
 | 
					            else if (std::holds_alternative<create_node_error>(new_node))
 | 
				
			||||||
 | 
					                switch (std::get<create_node_error>(new_node)) {
 | 
				
			||||||
 | 
					                case create_node_error::INVALID_NAME: return cbk(dto::Responses::get_badreq_res("Invalid name"));
 | 
				
			||||||
 | 
					                case create_node_error::INVALID_PARENT: return cbk(dto::Responses::get_badreq_res("Invalid parent"));
 | 
				
			||||||
 | 
					                case create_node_error::FILE_PARENT: return cbk(dto::Responses::get_badreq_res("Parent is file"));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            else {
 | 
				
			||||||
 | 
					                auto tuple = std::get<std::tuple<bool, uint64_t>>(new_node);
 | 
				
			||||||
 | 
					                cbk(dto::Responses::get_node_exists_res(std::get<1>(tuple), std::get<0>(tuple)));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        } catch (const std::exception&) {
 | 
					        } catch (const std::exception&) {
 | 
				
			||||||
            cbk(dto::Responses::get_badreq_res("Validation error"));
 | 
					            cbk(dto::Responses::get_badreq_res("Validation error"));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -152,12 +368,27 @@ namespace api {
 | 
				
			|||||||
        else if (inode->getValueOfParentId() == 0)
 | 
					        else if (inode->getValueOfParentId() == 0)
 | 
				
			||||||
            cbk(dto::Responses::get_badreq_res("Can't delete root"));
 | 
					            cbk(dto::Responses::get_badreq_res("Can't delete root"));
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
             delete_node(*inode);
 | 
					            auto chan = std::make_shared<msd::channel<std::string>>();
 | 
				
			||||||
            cbk(dto::Responses::get_success_res());
 | 
					            std::string("Waiting in queue...\n") >> (*chan);
 | 
				
			||||||
 | 
					            get_delete_loop()->queueInLoop([chan, inode=*inode]{
 | 
				
			||||||
 | 
					                delete_node(inode, *chan);
 | 
				
			||||||
 | 
					                chan->close();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            cbk(drogon::HttpResponse::newStreamResponse([chan](char* buf, std::size_t size) -> std::size_t{
 | 
				
			||||||
 | 
					                if (buf == nullptr) return 0;
 | 
				
			||||||
 | 
					                if (chan->closed() && chan->empty()) return 0;
 | 
				
			||||||
 | 
					                std::string buffer;
 | 
				
			||||||
 | 
					                buffer << *chan;
 | 
				
			||||||
 | 
					                if (buffer.empty()) return 0;
 | 
				
			||||||
 | 
					                std::size_t read = std::min(size, buffer.size());
 | 
				
			||||||
 | 
					                std::memcpy(buf, buffer.data(), read); // NOLINT(bugprone-not-null-terminated-result)
 | 
				
			||||||
 | 
					                return read;
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void fs::upload(req_type req, cbk_type cbk, uint64_t node) {
 | 
					    void fs::upload(req_type req, cbk_type cbk, uint64_t node) {
 | 
				
			||||||
 | 
					        constexpr int image_height = 256;
 | 
				
			||||||
        db::User user = dto::get_user(req);
 | 
					        db::User user = dto::get_user(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        auto inode = get_node_and_validate(user, node);
 | 
					        auto inode = get_node_and_validate(user, node);
 | 
				
			||||||
@@ -178,13 +409,73 @@ namespace api {
 | 
				
			|||||||
        p /= std::to_string(inode->getValueOfId());
 | 
					        p /= std::to_string(inode->getValueOfId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        file.saveAs(p.string());
 | 
					        file.saveAs(p.string());
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (file.fileLength() > 100 * 1024 * 1024) throw std::exception();
 | 
				
			||||||
 | 
					            std::filesystem::path filename(inode->getValueOfName());
 | 
				
			||||||
 | 
					            const std::string& mime = mime_type_map.at(filename.extension().string());
 | 
				
			||||||
 | 
					            if (!mime.starts_with("image")) throw std::exception();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            cv::_InputArray image_arr(file.fileData(), (int) file.fileLength());
 | 
				
			||||||
 | 
					            cv::Mat image = cv::imdecode(image_arr, cv::IMREAD_COLOR);
 | 
				
			||||||
 | 
					            if (!image.empty()) {
 | 
				
			||||||
 | 
					                float h_ration = ((float) image_height) / ((float) image.rows);
 | 
				
			||||||
 | 
					                cv::Mat preview;
 | 
				
			||||||
 | 
					                cv::resize(image, preview, cv::Size((int) (((float) image.cols) * h_ration), image_height), 0, 0, cv::INTER_AREA);
 | 
				
			||||||
 | 
					                cv::imwrite(p.string() + "_preview.png", preview);
 | 
				
			||||||
 | 
					                inode->setHasPreview(1);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (const std::exception&) {}
 | 
				
			||||||
        inode->setSize(file.fileLength());
 | 
					        inode->setSize(file.fileLength());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        db::MapperInode inode_mapper(drogon::app().getDbClient());
 | 
					        db::MapperInode inode_mapper(drogon::app().getDbClient());
 | 
				
			||||||
        inode_mapper.update(*inode);
 | 
					        inode_mapper.update(*inode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cbk(dto::Responses::get_success_res());
 | 
					        cbk(dto::Responses::get_success_res());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void fs::create_zip(req_type req, cbk_type cbk) {
 | 
				
			||||||
 | 
					        db::User user = dto::get_user(req);
 | 
				
			||||||
 | 
					        Json::Value& json = *req->jsonObject();
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (!json.isMember("nodes")) throw std::exception();
 | 
				
			||||||
 | 
					            Json::Value node_arr = json["nodes"];
 | 
				
			||||||
 | 
					            if (!node_arr.isArray()) throw std::exception();
 | 
				
			||||||
 | 
					            std::vector<uint64_t> node_ids;
 | 
				
			||||||
 | 
					            for (const auto& node : node_arr)
 | 
				
			||||||
 | 
					                node_ids.push_back(node.asUInt64());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            std::vector<db::INode> nodes;
 | 
				
			||||||
 | 
					            std::transform(node_ids.begin(), node_ids.end(), std::back_inserter(nodes), [&user](uint64_t node) {
 | 
				
			||||||
 | 
					                return api::fs::get_node_and_validate(user, node).value();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            std::string key = join_string(node_ids.begin(), node_ids.end());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (zip_to_temp_map.contains(key)) return cbk(dto::Responses::get_create_zip_done_res());
 | 
				
			||||||
 | 
					            if (in_progress_zips.contains(key)) {
 | 
				
			||||||
 | 
					                auto progress = in_progress_zips.at(key);
 | 
				
			||||||
 | 
					                return cbk(dto::Responses::get_create_zip_done_res(std::get<1>(progress), std::get<2>(progress)));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            uint64_t size = 0;
 | 
				
			||||||
 | 
					            for (const auto& node : nodes) size += calc_total_size(node);
 | 
				
			||||||
 | 
					            std::string file_name = "./temp/fs_" + std::to_string(next_temp_id++) + ".zip";
 | 
				
			||||||
 | 
					            in_progress_zips.emplace(key, std::make_tuple(file_name, 0, size));
 | 
				
			||||||
 | 
					            get_zip_loop()->queueInLoop([key = std::move(key), nodes = std::move(nodes), file_name = std::move(file_name)]{
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    struct zip_t* zip = zip_open(file_name.c_str(), ZIP_DEFAULT_COMPRESSION_LEVEL, 'w');
 | 
				
			||||||
 | 
					                    for (const db::INode& node : nodes)
 | 
				
			||||||
 | 
					                        add_to_zip(zip, key, node, "");
 | 
				
			||||||
 | 
					                    zip_close(zip);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                zip_to_temp_map.emplace(key, file_name);
 | 
				
			||||||
 | 
					                in_progress_zips.erase(key);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return cbk(dto::Responses::get_create_zip_done_res(0, size));
 | 
				
			||||||
 | 
					        } catch (const std::exception&) {
 | 
				
			||||||
 | 
					            cbk(dto::Responses::get_badreq_res("Validation error"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void fs::download(req_type req, cbk_type cbk) {
 | 
					    void fs::download(req_type req, cbk_type cbk) {
 | 
				
			||||||
        db::User user = dto::get_user(req);
 | 
					        db::User user = dto::get_user(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -193,19 +484,92 @@ namespace api {
 | 
				
			|||||||
            cbk(dto::Responses::get_badreq_res("Invalid node"));
 | 
					            cbk(dto::Responses::get_badreq_res("Invalid node"));
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        auto inode =  get_node_and_validate(user, *node_id);
 | 
					        auto inode = get_node_and_validate(user, *node_id);
 | 
				
			||||||
        if (!inode.has_value()) {
 | 
					        if (!inode.has_value()) {
 | 
				
			||||||
            cbk(dto::Responses::get_badreq_res("Invalid node"));
 | 
					            cbk(dto::Responses::get_badreq_res("Invalid node"));
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (inode->getValueOfIsFile() != 0) {
 | 
				
			||||||
 | 
					            std::filesystem::path p("./files");
 | 
				
			||||||
 | 
					            p /= std::to_string(inode->getValueOfId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            cbk(drogon::HttpResponse::newFileResponse(
 | 
				
			||||||
 | 
					                    p.string(),
 | 
				
			||||||
 | 
					                    inode->getValueOfName()
 | 
				
			||||||
 | 
					            ));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                std::string key = std::to_string(inode->getValueOfId());
 | 
				
			||||||
 | 
					                std::string file = zip_to_temp_map.at(key);
 | 
				
			||||||
 | 
					                zip_to_temp_map.erase(key);
 | 
				
			||||||
 | 
					                cbk(drogon::HttpResponse::newFileResponse(
 | 
				
			||||||
 | 
					                        file,
 | 
				
			||||||
 | 
					                        inode->getValueOfName() + ".zip"
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					            } catch (const std::exception&) {
 | 
				
			||||||
 | 
					                cbk(dto::Responses::get_badreq_res("Invalid node"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void fs::download_multi(req_type req, cbk_type cbk) {
 | 
				
			||||||
 | 
					        db::User user = dto::get_user(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        auto node_ids_str = req->getOptionalParameter<std::string>("id");
 | 
				
			||||||
 | 
					        if (!node_ids_str.has_value())
 | 
				
			||||||
 | 
					            return cbk(dto::Responses::get_badreq_res("No nodes"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        std::stringstream node_ids_ss(*node_ids_str);
 | 
				
			||||||
 | 
					        std::string temp;
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            while (std::getline(node_ids_ss, temp, ','))
 | 
				
			||||||
 | 
					                if (!get_node_and_validate(user, std::stoull(temp)).has_value()) throw std::exception();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            std::string file = zip_to_temp_map.at(*node_ids_str);
 | 
				
			||||||
 | 
					            zip_to_temp_map.erase(*node_ids_str);
 | 
				
			||||||
 | 
					            cbk(drogon::HttpResponse::newFileResponse(
 | 
				
			||||||
 | 
					                    file,
 | 
				
			||||||
 | 
					                    "files.zip"
 | 
				
			||||||
 | 
					            ));
 | 
				
			||||||
 | 
					        } catch (const std::exception&) {
 | 
				
			||||||
 | 
					            cbk(dto::Responses::get_badreq_res("Invalid nodes"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void fs::download_preview(req_type req, cbk_type cbk, uint64_t node) {
 | 
				
			||||||
 | 
					        db::User user = dto::get_user(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        auto inode = get_node_and_validate(user, node);
 | 
				
			||||||
 | 
					        if (!inode.has_value())
 | 
				
			||||||
 | 
					            return cbk(dto::Responses::get_badreq_res("Unknown node"));
 | 
				
			||||||
 | 
					        if (inode->getValueOfHasPreview() == 0)
 | 
				
			||||||
 | 
					            return cbk(dto::Responses::get_badreq_res("No preview"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        std::filesystem::path p("./files");
 | 
					        std::filesystem::path p("./files");
 | 
				
			||||||
 | 
					        p /= std::to_string(inode->getValueOfId()) + "_preview.png";
 | 
				
			||||||
 | 
					        std::ifstream file(p, std::ios::in | std::ios::binary);
 | 
				
			||||||
 | 
					        std::vector<uint8_t> image((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cbk(dto::Responses::get_download_base64_res("data:image/png;base64," + Botan::base64_encode(image)));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void fs::get_type(req_type req, cbk_type cbk, uint64_t node){
 | 
				
			||||||
 | 
					        db::User user = dto::get_user(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        auto inode = get_node_and_validate(user, node);
 | 
				
			||||||
 | 
					        if (!inode.has_value())
 | 
				
			||||||
 | 
					            return cbk(dto::Responses::get_badreq_res("Unknown node"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        std::filesystem::path p("./files"), name(inode->getValueOfName());
 | 
				
			||||||
        p /= std::to_string(inode->getValueOfId());
 | 
					        p /= std::to_string(inode->getValueOfId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cbk(drogon::HttpResponse::newFileResponse(
 | 
					        try {
 | 
				
			||||||
            p.string(),
 | 
					            cbk(dto::Responses::get_type_res(mime_type_map.at(name.extension().string())));
 | 
				
			||||||
            inode->getValueOfName()
 | 
					        } catch (const std::exception&) {
 | 
				
			||||||
        ));
 | 
					            cbk(dto::Responses::get_badreq_res("Invalid file type"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#pragma clang diagnostic pop
 | 
					#pragma clang diagnostic pop
 | 
				
			||||||
@@ -18,9 +18,10 @@ namespace api {
 | 
				
			|||||||
    void user::delete_user(req_type req, cbk_type cbk) {
 | 
					    void user::delete_user(req_type req, cbk_type cbk) {
 | 
				
			||||||
        db::MapperUser user_mapper(drogon::app().getDbClient());
 | 
					        db::MapperUser user_mapper(drogon::app().getDbClient());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        msd::channel<std::string> chan;
 | 
				
			||||||
        db::User user = dto::get_user(req);
 | 
					        db::User user = dto::get_user(req);
 | 
				
			||||||
        auth::revoke_all(user);
 | 
					        auth::revoke_all(user);
 | 
				
			||||||
        fs::delete_node((fs::get_node(user.getValueOfRootId())).value(), true);
 | 
					        fs::delete_node((fs::get_node(user.getValueOfRootId())).value(), chan, true);
 | 
				
			||||||
        user_mapper.deleteOne(user);
 | 
					        user_mapper.deleteOne(user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cbk(dto::Responses::get_success_res());
 | 
					        cbk(dto::Responses::get_success_res());
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,9 +6,9 @@
 | 
				
			|||||||
#include <drogon/utils/coroutine.h>
 | 
					#include <drogon/utils/coroutine.h>
 | 
				
			||||||
#include <drogon/drogon.h>
 | 
					#include <drogon/drogon.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "model/Inode.h"
 | 
					#include "Inode.h"
 | 
				
			||||||
#include "model/Tokens.h"
 | 
					#include "Tokens.h"
 | 
				
			||||||
#include "model/User.h"
 | 
					#include "User.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const std::string jwt_secret = "CUM";
 | 
					const std::string jwt_secret = "CUM";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,14 +22,24 @@ namespace dto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    namespace Responses {
 | 
					    namespace Responses {
 | 
				
			||||||
        struct GetUsersEntry {
 | 
					        struct GetUsersEntry {
 | 
				
			||||||
            GetUsersEntry(int id, bool gitlab, bool tfa, std::string name, db::UserRole role)
 | 
					            GetUsersEntry(uint64_t id, bool gitlab, bool tfa, std::string name, db::UserRole role)
 | 
				
			||||||
                : id(id), gitlab(gitlab), tfa(tfa), name(std::move(name)), role(role) {}
 | 
					                : id(id), gitlab(gitlab), tfa(tfa), name(std::move(name)), role(role) {}
 | 
				
			||||||
            int id;
 | 
					            uint64_t id;
 | 
				
			||||||
            bool gitlab, tfa;
 | 
					            bool gitlab, tfa;
 | 
				
			||||||
            std::string name;
 | 
					            std::string name;
 | 
				
			||||||
            db::UserRole role;
 | 
					            db::UserRole role;
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        struct GetNodeEntry {
 | 
				
			||||||
 | 
					            explicit GetNodeEntry(const db::INode& node) : id(node.getValueOfId()), name(node.getValueOfName()), is_file(node.getValueOfIsFile() != 0), has_preview(node.getValueOfHasPreview() != 0), parent(node.getParentId()) {
 | 
				
			||||||
 | 
					                if (node.getValueOfIsFile() != 0) size = node.getValueOfSize();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            uint64_t id, size;
 | 
				
			||||||
 | 
					            std::string name;
 | 
				
			||||||
 | 
					            bool is_file, has_preview;
 | 
				
			||||||
 | 
					            std::shared_ptr<uint64_t> parent;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        drogon::HttpResponsePtr get_error_res(drogon::HttpStatusCode, const std::string &msg);
 | 
					        drogon::HttpResponsePtr get_error_res(drogon::HttpStatusCode, const std::string &msg);
 | 
				
			||||||
        drogon::HttpResponsePtr get_success_res();
 | 
					        drogon::HttpResponsePtr get_success_res();
 | 
				
			||||||
        drogon::HttpResponsePtr get_success_res(Json::Value &);
 | 
					        drogon::HttpResponsePtr get_success_res(Json::Value &);
 | 
				
			||||||
@@ -46,10 +56,13 @@ namespace dto {
 | 
				
			|||||||
        drogon::HttpResponsePtr get_admin_users_res(const std::vector<GetUsersEntry>& users);
 | 
					        drogon::HttpResponsePtr get_admin_users_res(const std::vector<GetUsersEntry>& users);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        drogon::HttpResponsePtr get_root_res(uint64_t root);
 | 
					        drogon::HttpResponsePtr get_root_res(uint64_t root);
 | 
				
			||||||
        drogon::HttpResponsePtr get_node_folder_res(uint64_t id, const std::string& name, const std::shared_ptr<uint64_t>& parent, const std::vector<uint64_t>& children);
 | 
					        drogon::HttpResponsePtr get_node_res(const GetNodeEntry& node, const std::vector<GetNodeEntry>& children);
 | 
				
			||||||
        drogon::HttpResponsePtr get_node_file_res(uint64_t id, const std::string& name, const std::shared_ptr<uint64_t>& parent, uint64_t size);
 | 
					 | 
				
			||||||
        drogon::HttpResponsePtr get_path_res(const std::string& path);
 | 
					 | 
				
			||||||
        drogon::HttpResponsePtr get_new_node_res(uint64_t id);
 | 
					        drogon::HttpResponsePtr get_new_node_res(uint64_t id);
 | 
				
			||||||
 | 
					        drogon::HttpResponsePtr get_node_exists_res(uint64_t id, bool file);
 | 
				
			||||||
 | 
					        drogon::HttpResponsePtr get_download_base64_res(const std::string& data);
 | 
				
			||||||
 | 
					        drogon::HttpResponsePtr get_type_res(const std::string& type);
 | 
				
			||||||
 | 
					        drogon::HttpResponsePtr get_create_zip_done_res();
 | 
				
			||||||
 | 
					        drogon::HttpResponsePtr get_create_zip_done_res(uint64_t progress, uint64_t total);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,30 +63,24 @@ namespace dto::Responses {
 | 
				
			|||||||
        return get_success_res(json);
 | 
					        return get_success_res(json);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    drogon::HttpResponsePtr get_node_folder_res(uint64_t id, const std::string &name, const std::shared_ptr<uint64_t> &parent, const std::vector<uint64_t> &children) {
 | 
					    Json::Value parse_node(const GetNodeEntry& node) {
 | 
				
			||||||
        Json::Value json;
 | 
					        Json::Value json;
 | 
				
			||||||
        json["id"] = id;
 | 
					        json["id"] = node.id;
 | 
				
			||||||
        json["name"] = name;
 | 
					        json["name"] = node.name;
 | 
				
			||||||
        json["isFile"] = false;
 | 
					        json["isFile"] = node.is_file;
 | 
				
			||||||
        json["parent"] = (parent != nullptr) ? *parent : Json::Value::nullSingleton();
 | 
					        json["preview"] = node.has_preview;
 | 
				
			||||||
        for (uint64_t child : children)
 | 
					        json["parent"] = (node.parent != nullptr) ? *node.parent : Json::Value::nullSingleton();
 | 
				
			||||||
            json["children"].append(child);
 | 
					        if (node.is_file) json["size"] = node.size;
 | 
				
			||||||
        return get_success_res(json);
 | 
					        return json;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    drogon::HttpResponsePtr get_node_file_res(uint64_t id, const std::string &name, const std::shared_ptr<uint64_t> &parent, uint64_t size) {
 | 
					    drogon::HttpResponsePtr get_node_res(const GetNodeEntry& node, const std::vector<GetNodeEntry>& children) {
 | 
				
			||||||
        Json::Value json;
 | 
					        Json::Value json = parse_node(node);
 | 
				
			||||||
        json["id"] = id;
 | 
					        if (!node.is_file) {
 | 
				
			||||||
        json["name"] = name;
 | 
					            json["children"] = Json::Value(Json::arrayValue);
 | 
				
			||||||
        json["isFile"] = true;
 | 
					            for (const GetNodeEntry& child : children)
 | 
				
			||||||
        json["parent"] = (parent != nullptr) ? *parent : Json::Value::nullSingleton();
 | 
					                json["children"].append(parse_node(child));
 | 
				
			||||||
        json["size"] = size;
 | 
					        }
 | 
				
			||||||
        return get_success_res(json);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    drogon::HttpResponsePtr get_path_res(const std::string& path) {
 | 
					 | 
				
			||||||
        Json::Value json;
 | 
					 | 
				
			||||||
        json["path"] = path;
 | 
					 | 
				
			||||||
        return get_success_res(json);
 | 
					        return get_success_res(json);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -95,4 +89,38 @@ namespace dto::Responses {
 | 
				
			|||||||
        json["id"] = id;
 | 
					        json["id"] = id;
 | 
				
			||||||
        return get_success_res(json);
 | 
					        return get_success_res(json);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    drogon::HttpResponsePtr get_node_exists_res(uint64_t id, bool file) {
 | 
				
			||||||
 | 
					        Json::Value json;
 | 
				
			||||||
 | 
					        json["id"] = id;
 | 
				
			||||||
 | 
					        json["exists"] = true;
 | 
				
			||||||
 | 
					        json["isFile"] = file;
 | 
				
			||||||
 | 
					        return get_success_res(json);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    drogon::HttpResponsePtr get_download_base64_res(const std::string &data) {
 | 
				
			||||||
 | 
					        Json::Value json;
 | 
				
			||||||
 | 
					        json["data"] = data;
 | 
				
			||||||
 | 
					        return get_success_res(json);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    drogon::HttpResponsePtr get_type_res(const std::string &type) {
 | 
				
			||||||
 | 
					        Json::Value json;
 | 
				
			||||||
 | 
					        json["type"] = type;
 | 
				
			||||||
 | 
					        return get_success_res(json);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    drogon::HttpResponsePtr get_create_zip_done_res() {
 | 
				
			||||||
 | 
					        Json::Value json;
 | 
				
			||||||
 | 
					        json["done"] = true;
 | 
				
			||||||
 | 
					        return get_success_res(json);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    drogon::HttpResponsePtr get_create_zip_done_res(uint64_t progress, uint64_t total) {
 | 
				
			||||||
 | 
					        Json::Value json;
 | 
				
			||||||
 | 
					        json["done"] = false;
 | 
				
			||||||
 | 
					        json["progress"] = progress;
 | 
				
			||||||
 | 
					        json["total"] = total;
 | 
				
			||||||
 | 
					        return get_success_res(json);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ void cleanup_tokens(db::MapperToken& mapper) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
void Login::doFilter(const drogon::HttpRequestPtr& req, drogon::FilterCallback&& cb, drogon::FilterChainCallback&& ccb) {
 | 
					void Login::doFilter(const drogon::HttpRequestPtr& req, drogon::FilterCallback&& cb, drogon::FilterChainCallback&& ccb) {
 | 
				
			||||||
    std::string token_str;
 | 
					    std::string token_str;
 | 
				
			||||||
    if (req->path() == "/api/fs/download") {
 | 
					    if (req->path() == "/api/fs/download" || req->path() == "/api/fs/download_multi") {
 | 
				
			||||||
        token_str = req->getParameter("jwtToken");
 | 
					        token_str = req->getParameter("jwtToken");
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        std::string auth_header = req->getHeader("Authorization");
 | 
					        std::string auth_header = req->getHeader("Authorization");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@
 | 
				
			|||||||
#include <fstream>
 | 
					#include <fstream>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <drogon/drogon.h>
 | 
					#include <drogon/drogon.h>
 | 
				
			||||||
#include <curl/curl.h>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "dto/dto.h"
 | 
					#include "dto/dto.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,6 +13,9 @@ void cleanup() {
 | 
				
			|||||||
    std::cout << "Cleanup up uploads...";
 | 
					    std::cout << "Cleanup up uploads...";
 | 
				
			||||||
    std::filesystem::remove_all("uploads");
 | 
					    std::filesystem::remove_all("uploads");
 | 
				
			||||||
    std::cout << " [Done]" << std::endl;
 | 
					    std::cout << " [Done]" << std::endl;
 | 
				
			||||||
 | 
					    std::cout << "Removing temp folder..." << std::flush;
 | 
				
			||||||
 | 
					    std::filesystem::remove_all("temp");
 | 
				
			||||||
 | 
					    std::cout << " [Done]" << std::endl;
 | 
				
			||||||
    std::cout << "Goodbye!" << std::endl;
 | 
					    std::cout << "Goodbye!" << std::endl;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,9 +40,6 @@ int main(int argc, char* argv[]) {
 | 
				
			|||||||
    if (std::find(args.begin(), args.end(), "--dev") != args.end()) dev_mode = true;
 | 
					    if (std::find(args.begin(), args.end(), "--dev") != args.end()) dev_mode = true;
 | 
				
			||||||
    if (dev_mode) std::cout << "Starting in development mode" << std::endl;
 | 
					    if (dev_mode) std::cout << "Starting in development mode" << std::endl;
 | 
				
			||||||
    std::cout << "Setting up..." << std::endl;
 | 
					    std::cout << "Setting up..." << std::endl;
 | 
				
			||||||
    std::cout << "Initializing curl..." << std::flush;
 | 
					 | 
				
			||||||
    curl_global_init(CURL_GLOBAL_ALL);
 | 
					 | 
				
			||||||
    std::cout << " [Done]" << std::endl;
 | 
					 | 
				
			||||||
    if (!std::filesystem::exists("files")) {
 | 
					    if (!std::filesystem::exists("files")) {
 | 
				
			||||||
        std::cout << "Creating files..." << std::flush;
 | 
					        std::cout << "Creating files..." << std::flush;
 | 
				
			||||||
        std::filesystem::create_directory("files");
 | 
					        std::filesystem::create_directory("files");
 | 
				
			||||||
@@ -51,6 +50,15 @@ int main(int argc, char* argv[]) {
 | 
				
			|||||||
        std::filesystem::create_directory("logs");
 | 
					        std::filesystem::create_directory("logs");
 | 
				
			||||||
        std::cout << " [Done]" << std::endl;
 | 
					        std::cout << " [Done]" << std::endl;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (std::filesystem::exists("temp")) {
 | 
				
			||||||
 | 
					        std::cout << "Removing existing temp folder..." << std::flush;
 | 
				
			||||||
 | 
					        std::filesystem::remove_all("temp");
 | 
				
			||||||
 | 
					        std::cout << " [Done]" << std::endl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    std::cout << "Creating temp folder..." << std::flush;
 | 
				
			||||||
 | 
					    std::filesystem::create_directory("temp");
 | 
				
			||||||
 | 
					    std::cout << " [Done]" << std::endl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    auto* loop = drogon::app().getLoop();
 | 
					    auto* loop = drogon::app().getLoop();
 | 
				
			||||||
    loop->queueInLoop([]{
 | 
					    loop->queueInLoop([]{
 | 
				
			||||||
@@ -80,7 +88,8 @@ int main(int argc, char* argv[]) {
 | 
				
			|||||||
                        "  'name' TEXT,\n"
 | 
					                        "  'name' TEXT,\n"
 | 
				
			||||||
                        "  'parent_id' INTEGER,\n"
 | 
					                        "  'parent_id' INTEGER,\n"
 | 
				
			||||||
                        "  'owner_id' INTEGER NOT NULL,\n"
 | 
					                        "  'owner_id' INTEGER NOT NULL,\n"
 | 
				
			||||||
                        "  'size' INTEGER\n"
 | 
					                        "  'size' INTEGER,\n"
 | 
				
			||||||
 | 
					                        "  'has_preview' INTEGER NOT NULL\n"
 | 
				
			||||||
                        ")");
 | 
					                        ")");
 | 
				
			||||||
        std::cout << " [Done]" << std::endl;
 | 
					        std::cout << " [Done]" << std::endl;
 | 
				
			||||||
        std::cout << "Started!" << std::endl;
 | 
					        std::cout << "Started!" << std::endl;
 | 
				
			||||||
@@ -105,8 +114,12 @@ int main(int argc, char* argv[]) {
 | 
				
			|||||||
    Json::Value access_logger;
 | 
					    Json::Value access_logger;
 | 
				
			||||||
    access_logger["name"] = "drogon::plugin::AccessLogger";
 | 
					    access_logger["name"] = "drogon::plugin::AccessLogger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Json::Value smtp_mail;
 | 
				
			||||||
 | 
					    smtp_mail["name"] = "SMTPMail";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Json::Value config;
 | 
					    Json::Value config;
 | 
				
			||||||
    config["plugins"].append(access_logger);
 | 
					    config["plugins"].append(access_logger);
 | 
				
			||||||
 | 
					    config["plugins"].append(smtp_mail);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    drogon::app()
 | 
					    drogon::app()
 | 
				
			||||||
            .setClientMaxBodySize(std::numeric_limits<size_t>::max())
 | 
					            .setClientMaxBodySize(std::numeric_limits<size_t>::max())
 | 
				
			||||||
@@ -127,8 +140,10 @@ int main(int argc, char* argv[]) {
 | 
				
			|||||||
            .setIntSignalHandler(cleanup)
 | 
					            .setIntSignalHandler(cleanup)
 | 
				
			||||||
            .setTermSignalHandler(cleanup)
 | 
					            .setTermSignalHandler(cleanup)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            .addListener("0.0.0.0", 5678)
 | 
					            .enableRelaunchOnError()
 | 
				
			||||||
            .setThreadNum(2);
 | 
					
 | 
				
			||||||
 | 
					            .addListener("0.0.0.0", 2345)
 | 
				
			||||||
 | 
					            .setThreadNum(8);
 | 
				
			||||||
    std::cout << "Setup done!" << std::endl;
 | 
					    std::cout << "Setup done!" << std::endl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    drogon::app().run();
 | 
					    drogon::app().run();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								backend/vcpkg-configuration.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/vcpkg-configuration.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "default-registry": {
 | 
				
			||||||
 | 
					    "kind": "git",
 | 
				
			||||||
 | 
					    "repository": "https://github.com/microsoft/vcpkg.git",
 | 
				
			||||||
 | 
					    "baseline": "927006b24c3a28dfd8aa0ec5f8ce43098480a7f1"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "registries": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "kind": "filesystem",
 | 
				
			||||||
 | 
					      "baseline": "default",
 | 
				
			||||||
 | 
					      "path": "./vcpkg_reg",
 | 
				
			||||||
 | 
					      "packages": [ "drogon" ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -7,11 +7,15 @@
 | 
				
			|||||||
      "name": "drogon",
 | 
					      "name": "drogon",
 | 
				
			||||||
      "features": ["orm", "sqlite3"]
 | 
					      "features": ["orm", "sqlite3"]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "name": "opencv4",
 | 
				
			||||||
 | 
					      "default-features": false,
 | 
				
			||||||
 | 
					      "features": ["tiff", "png", "jpeg", "webp", "openexr"]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "jwt-cpp",
 | 
					    "jwt-cpp",
 | 
				
			||||||
    "botan",
 | 
					    "botan",
 | 
				
			||||||
    "curl",
 | 
					 | 
				
			||||||
    "nayuki-qr-code-generator",
 | 
					    "nayuki-qr-code-generator",
 | 
				
			||||||
    "lodepng",
 | 
					    "openssl",
 | 
				
			||||||
    "openssl"
 | 
					    "kubazip"
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								backend/vcpkg_reg/ports/drogon/drogon_config.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend/vcpkg_reg/ports/drogon/drogon_config.patch
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					diff --git a/cmake/templates/DrogonConfig.cmake.in b/cmake/templates/DrogonConfig.cmake.in
 | 
				
			||||||
 | 
					index a21122a..6367259 100644
 | 
				
			||||||
 | 
					--- a/cmake/templates/DrogonConfig.cmake.in
 | 
				
			||||||
 | 
					+++ b/cmake/templates/DrogonConfig.cmake.in
 | 
				
			||||||
 | 
					@@ -19,7 +19,7 @@ find_dependency(UUID REQUIRED)
 | 
				
			||||||
 | 
					 endif(NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND NOT WIN32)
 | 
				
			||||||
 | 
					 find_dependency(ZLIB REQUIRED)
 | 
				
			||||||
 | 
					 if(@pg_FOUND@)
 | 
				
			||||||
 | 
					-find_dependency(pg)
 | 
				
			||||||
 | 
					+find_dependency(PostgreSQL)
 | 
				
			||||||
 | 
					 endif()
 | 
				
			||||||
 | 
					 if(@SQLite3_FOUND@)
 | 
				
			||||||
 | 
					 find_dependency(SQLite3)
 | 
				
			||||||
							
								
								
									
										61
									
								
								backend/vcpkg_reg/ports/drogon/portfile.cmake
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								backend/vcpkg_reg/ports/drogon/portfile.cmake
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					vcpkg_from_github(
 | 
				
			||||||
 | 
					    OUT_SOURCE_PATH SOURCE_PATH
 | 
				
			||||||
 | 
					    REPO an-tao/drogon
 | 
				
			||||||
 | 
					    REF v1.8.0
 | 
				
			||||||
 | 
					    SHA512 a834d937e3719059223d9bf19d777dbc92eaf09c5c9c44b5a742bfefcbcd95a146a6568cef8c058050fb87e330f221434ffe784dfa29a49de12b031f86ab1a33
 | 
				
			||||||
 | 
					    HEAD_REF master
 | 
				
			||||||
 | 
					    PATCHES
 | 
				
			||||||
 | 
					        vcpkg.patch
 | 
				
			||||||
 | 
					        drogon_config.patch
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					vcpkg_check_features(
 | 
				
			||||||
 | 
					    OUT_FEATURE_OPTIONS FEATURE_OPTIONS
 | 
				
			||||||
 | 
					    FEATURES
 | 
				
			||||||
 | 
					        ctl      BUILD_CTL
 | 
				
			||||||
 | 
					        mysql    BUILD_MYSQL
 | 
				
			||||||
 | 
					        orm      BUILD_ORM
 | 
				
			||||||
 | 
					        postgres BUILD_POSTGRESQL
 | 
				
			||||||
 | 
					        postgres LIBPQ_BATCH_MODE
 | 
				
			||||||
 | 
					        redis    BUILD_REDIS
 | 
				
			||||||
 | 
					        sqlite3  BUILD_SQLITE
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "dynamic" BUILD_DROGON_SHARED)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					vcpkg_cmake_configure(
 | 
				
			||||||
 | 
					    SOURCE_PATH "${SOURCE_PATH}"
 | 
				
			||||||
 | 
					    DISABLE_PARALLEL_CONFIGURE
 | 
				
			||||||
 | 
					    OPTIONS
 | 
				
			||||||
 | 
					        -DBUILD_SHARED_LIBS=${BUILD_DROGON_SHARED}
 | 
				
			||||||
 | 
					        -DBUILD_EXAMPLES=OFF
 | 
				
			||||||
 | 
					        -DCMAKE_DISABLE_FIND_PACKAGE_Boost=ON
 | 
				
			||||||
 | 
					        ${FEATURE_OPTIONS}
 | 
				
			||||||
 | 
					    MAYBE_UNUSED_VARIABLES
 | 
				
			||||||
 | 
					        CMAKE_DISABLE_FIND_PACKAGE_Boost
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					vcpkg_cmake_install(ADD_BIN_TO_PATH)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Fix CMake files
 | 
				
			||||||
 | 
					vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/Drogon)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					vcpkg_fixup_pkgconfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy drogon_ctl
 | 
				
			||||||
 | 
					if("ctl" IN_LIST FEATURES)
 | 
				
			||||||
 | 
					    vcpkg_copy_tools(TOOL_NAMES drogon_ctl AUTO_CLEAN)
 | 
				
			||||||
 | 
					endif()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Remove includes in debug
 | 
				
			||||||
 | 
					file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")
 | 
				
			||||||
 | 
					file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share")
 | 
				
			||||||
 | 
					if(VCPKG_LIBRARY_LINKAGE STREQUAL "static")
 | 
				
			||||||
 | 
					    file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/bin" "${CURRENT_PACKAGES_DIR}/debug/bin")
 | 
				
			||||||
 | 
					endif()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")
 | 
				
			||||||
 | 
					file(INSTALL "${SOURCE_PATH}/LICENSE" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy pdb files
 | 
				
			||||||
 | 
					vcpkg_copy_pdbs()
 | 
				
			||||||
							
								
								
									
										4
									
								
								backend/vcpkg_reg/ports/drogon/usage
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								backend/vcpkg_reg/ports/drogon/usage
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					The package drogon provides CMake targets:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    find_package(Drogon CONFIG REQUIRED)
 | 
				
			||||||
 | 
					    target_link_libraries(main PRIVATE Drogon::Drogon)
 | 
				
			||||||
							
								
								
									
										92
									
								
								backend/vcpkg_reg/ports/drogon/vcpkg.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								backend/vcpkg_reg/ports/drogon/vcpkg.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "drogon",
 | 
				
			||||||
 | 
					  "version-semver": "1.8.0",
 | 
				
			||||||
 | 
					  "description": "A C++14/17 based HTTP web application framework running on Linux/macOS/Unix/Windows",
 | 
				
			||||||
 | 
					  "homepage": "https://github.com/an-tao/drogon",
 | 
				
			||||||
 | 
					  "documentation": "https://drogon.docsforge.com/master/overview/",
 | 
				
			||||||
 | 
					  "license": "MIT",
 | 
				
			||||||
 | 
					  "dependencies": [
 | 
				
			||||||
 | 
					    "brotli",
 | 
				
			||||||
 | 
					    "jsoncpp",
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "name": "libuuid",
 | 
				
			||||||
 | 
					      "platform": "!windows & !osx"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "trantor",
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "name": "vcpkg-cmake",
 | 
				
			||||||
 | 
					      "host": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "name": "vcpkg-cmake-config",
 | 
				
			||||||
 | 
					      "host": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "zlib"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "features": {
 | 
				
			||||||
 | 
					    "ctl": {
 | 
				
			||||||
 | 
					      "description": "Build drogon_ctl tool."
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "mysql": {
 | 
				
			||||||
 | 
					      "description": "Support reading and writing from/to MySQL databases.",
 | 
				
			||||||
 | 
					      "dependencies": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "name": "drogon",
 | 
				
			||||||
 | 
					          "features": [
 | 
				
			||||||
 | 
					            "orm"
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "name": "libmariadb",
 | 
				
			||||||
 | 
					          "features": [
 | 
				
			||||||
 | 
					            "iconv"
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          "platform": "osx"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "name": "libmariadb",
 | 
				
			||||||
 | 
					          "platform": "!osx"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "orm": {
 | 
				
			||||||
 | 
					      "description": "Build with object-relational mapping support."
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "postgres": {
 | 
				
			||||||
 | 
					      "description": "Support reading and writing from/to Postgres databases.",
 | 
				
			||||||
 | 
					      "dependencies": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "name": "drogon",
 | 
				
			||||||
 | 
					          "features": [
 | 
				
			||||||
 | 
					            "orm"
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "libpq"
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "redis": {
 | 
				
			||||||
 | 
					      "description": "Support reading and writing from/to Redis databases.",
 | 
				
			||||||
 | 
					      "dependencies": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "name": "drogon",
 | 
				
			||||||
 | 
					          "features": [
 | 
				
			||||||
 | 
					            "orm"
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "hiredis"
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "sqlite3": {
 | 
				
			||||||
 | 
					      "description": "Support reading and writing from/to SQLite databases.",
 | 
				
			||||||
 | 
					      "dependencies": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "name": "drogon",
 | 
				
			||||||
 | 
					          "features": [
 | 
				
			||||||
 | 
					            "orm"
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "sqlite3"
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										53
									
								
								backend/vcpkg_reg/ports/drogon/vcpkg.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								backend/vcpkg_reg/ports/drogon/vcpkg.patch
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					diff --git a/CMakeLists.txt b/CMakeLists.txt
 | 
				
			||||||
 | 
					--- a/CMakeLists.txt
 | 
				
			||||||
 | 
					+++ b/CMakeLists.txt
 | 
				
			||||||
 | 
					@@ -120,9 +120,9 @@ if (WIN32)
 | 
				
			||||||
 | 
					         PRIVATE $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/third_party/mman-win32>)
 | 
				
			||||||
 | 
					 endif (WIN32)
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					-add_subdirectory(trantor)
 | 
				
			||||||
 | 
					+find_package(Trantor CONFIG REQUIRED)
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					-target_link_libraries(${PROJECT_NAME} PUBLIC trantor)
 | 
				
			||||||
 | 
					+target_link_libraries(${PROJECT_NAME} PUBLIC Trantor::Trantor)
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					 if(${CMAKE_SYSTEM_NAME} STREQUAL "Haiku")
 | 
				
			||||||
 | 
					     target_link_libraries(${PROJECT_NAME} PRIVATE network)
 | 
				
			||||||
 | 
					@@ -316,11 +316,10 @@ endif (NOT WIN32)
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					 if (BUILD_POSTGRESQL)
 | 
				
			||||||
 | 
					     # find postgres
 | 
				
			||||||
 | 
					-    find_package(pg)
 | 
				
			||||||
 | 
					-    if (pg_FOUND)
 | 
				
			||||||
 | 
					-        message(STATUS "libpq inc path:" ${PG_INCLUDE_DIRS})
 | 
				
			||||||
 | 
					-        message(STATUS "libpq lib:" ${PG_LIBRARIES})
 | 
				
			||||||
 | 
					-        target_link_libraries(${PROJECT_NAME} PRIVATE pg_lib)
 | 
				
			||||||
 | 
					+    find_package(PostgreSQL REQUIRED)
 | 
				
			||||||
 | 
					+    if(PostgreSQL_FOUND)
 | 
				
			||||||
 | 
					+      set(pg_FOUND true)
 | 
				
			||||||
 | 
					+      target_link_libraries(${PROJECT_NAME} PRIVATE PostgreSQL::PostgreSQL)
 | 
				
			||||||
 | 
					         set(DROGON_SOURCES
 | 
				
			||||||
 | 
					             ${DROGON_SOURCES}
 | 
				
			||||||
 | 
					             orm_lib/src/postgresql_impl/PostgreSQLResultImpl.cc)
 | 
				
			||||||
 | 
					@@ -348,7 +348,7 @@ if (BUILD_POSTGRESQL)
 | 
				
			||||||
 | 
					                 ${private_headers}
 | 
				
			||||||
 | 
					                 orm_lib/src/postgresql_impl/PgConnection.h)
 | 
				
			||||||
 | 
					         endif (libpq_supports_batch)
 | 
				
			||||||
 | 
					-    endif (pg_FOUND)
 | 
				
			||||||
 | 
					+    endif (PostgreSQL_FOUND)
 | 
				
			||||||
 | 
					 endif (BUILD_POSTGRESQL)
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					 if (BUILD_MYSQL)
 | 
				
			||||||
 | 
					diff --git a/drogon_ctl/CMakeLists.txt b/drogon_ctl/CMakeLists.txt
 | 
				
			||||||
 | 
					index 9f2f1e7..09871f8 100755
 | 
				
			||||||
 | 
					--- a/drogon_ctl/CMakeLists.txt
 | 
				
			||||||
 | 
					+++ b/drogon_ctl/CMakeLists.txt
 | 
				
			||||||
 | 
					@@ -19,7 +19,7 @@ add_executable(_drogon_ctl
 | 
				
			||||||
 | 
					 target_link_libraries(_drogon_ctl ${PROJECT_NAME})
 | 
				
			||||||
 | 
					 if (WIN32 AND BUILD_SHARED_LIBS)
 | 
				
			||||||
 | 
					   set(DROGON_FILE $<TARGET_FILE:drogon>)
 | 
				
			||||||
 | 
					-  set(TRANTOR_FILE $<TARGET_FILE:trantor>)
 | 
				
			||||||
 | 
					+  set(TRANTOR_FILE $<TARGET_FILE:Trantor::Trantor>)
 | 
				
			||||||
 | 
					   add_custom_command(TARGET _drogon_ctl POST_BUILD
 | 
				
			||||||
 | 
					           COMMAND ${CMAKE_COMMAND}
 | 
				
			||||||
 | 
					           -DCTL_FILE=${DROGON_FILE}
 | 
				
			||||||
							
								
								
									
										8
									
								
								backend/vcpkg_reg/versions/baseline.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								backend/vcpkg_reg/versions/baseline.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "default": {
 | 
				
			||||||
 | 
					    "drogon": {
 | 
				
			||||||
 | 
					      "baseline": "1.8.0",
 | 
				
			||||||
 | 
					      "port-version": 0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								backend/vcpkg_reg/versions/d-/drogon.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								backend/vcpkg_reg/versions/d-/drogon.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "versions": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "version-semver": "1.8.0",
 | 
				
			||||||
 | 
					      "port-version": 0,
 | 
				
			||||||
 | 
					      "path": "$/ports/drogon"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,15 +1,15 @@
 | 
				
			|||||||
/* eslint-env node */
 | 
					/* eslint-env node */
 | 
				
			||||||
require("@rushstack/eslint-patch/modern-module-resolution");
 | 
					require('@rushstack/eslint-patch/modern-module-resolution');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  root: true,
 | 
						root: true,
 | 
				
			||||||
  extends: [
 | 
						extends: [
 | 
				
			||||||
    "plugin:vue/vue3-essential",
 | 
							'plugin:vue/vue3-essential',
 | 
				
			||||||
    "eslint:recommended",
 | 
							'eslint:recommended',
 | 
				
			||||||
    "@vue/eslint-config-typescript/recommended",
 | 
							'@vue/eslint-config-typescript/recommended',
 | 
				
			||||||
    "@vue/eslint-config-prettier",
 | 
							'@vue/eslint-config-prettier'
 | 
				
			||||||
  ],
 | 
						],
 | 
				
			||||||
  parserOptions: {
 | 
						parserOptions: {
 | 
				
			||||||
    ecmaVersion: "latest",
 | 
							ecmaVersion: 'latest'
 | 
				
			||||||
  },
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										7
									
								
								frontend/.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/.prettierrc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "tabWidth": 4,
 | 
				
			||||||
 | 
					    "useTabs": true,
 | 
				
			||||||
 | 
					    "singleQuote": true,
 | 
				
			||||||
 | 
					    "trailingComma": "none",
 | 
				
			||||||
 | 
					    "endOfLine": "lf"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2,9 +2,9 @@
 | 
				
			|||||||
<html lang="en">
 | 
					<html lang="en">
 | 
				
			||||||
  <head>
 | 
					  <head>
 | 
				
			||||||
    <meta charset="UTF-8" />
 | 
					    <meta charset="UTF-8" />
 | 
				
			||||||
    <link rel="icon" href="/favicon.ico" />
 | 
					    <link rel="icon" href="/favicon.svg" />
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||||
    <title>Vite App</title>
 | 
					    <title>MFileserver</title>
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body>
 | 
					  <body>
 | 
				
			||||||
    <div id="app"></div>
 | 
					    <div id="app"></div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,16 +4,16 @@
 | 
				
			|||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "license": "suck my dick",
 | 
					  "license": "suck my dick",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "dev": "vite build --outDir ../run/static --watch",
 | 
					    "dev": "vite build -c vite.dev.config.ts --mode development",
 | 
				
			||||||
    "build": "run-p type-check build-only",
 | 
					    "build": "run-p type-check build-only",
 | 
				
			||||||
    "build-only": "vite build",
 | 
					    "build-only": "vite build",
 | 
				
			||||||
    "type-check": "vue-tsc --noEmit",
 | 
					    "type-check": "vue-tsc --noEmit",
 | 
				
			||||||
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
 | 
					    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@vicons/carbon": "^0.12.0",
 | 
				
			||||||
 | 
					    "@vicons/ionicons5": "^0.12.0",
 | 
				
			||||||
    "axios": "^0.27.2",
 | 
					    "axios": "^0.27.2",
 | 
				
			||||||
    "class-transformer": "^0.5.1",
 | 
					 | 
				
			||||||
    "class-validator": "^0.13.2",
 | 
					 | 
				
			||||||
    "filesize": "^9.0.11",
 | 
					    "filesize": "^9.0.11",
 | 
				
			||||||
    "jwt-decode": "^3.1.2",
 | 
					    "jwt-decode": "^3.1.2",
 | 
				
			||||||
    "naive-ui": "^2.32.1",
 | 
					    "naive-ui": "^2.32.1",
 | 
				
			||||||
@@ -26,6 +26,7 @@
 | 
				
			|||||||
    "@rushstack/eslint-patch": "^1.1.4",
 | 
					    "@rushstack/eslint-patch": "^1.1.4",
 | 
				
			||||||
    "@types/node": "^18.7.14",
 | 
					    "@types/node": "^18.7.14",
 | 
				
			||||||
    "@vitejs/plugin-vue": "^3.0.1",
 | 
					    "@vitejs/plugin-vue": "^3.0.1",
 | 
				
			||||||
 | 
					    "@vitejs/plugin-vue-jsx": "^2.0.0",
 | 
				
			||||||
    "@vue/eslint-config-prettier": "^7.0.0",
 | 
					    "@vue/eslint-config-prettier": "^7.0.0",
 | 
				
			||||||
    "@vue/eslint-config-typescript": "^11.0.0",
 | 
					    "@vue/eslint-config-typescript": "^11.0.0",
 | 
				
			||||||
    "@vue/tsconfig": "^0.1.3",
 | 
					    "@vue/tsconfig": "^0.1.3",
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										1
									
								
								frontend/public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32"><path d="M28 20h-2v2h2v6H4v-6h2v-2H4a2.002 2.002 0 0 0-2 2v6a2.002 2.002 0 0 0 2 2h24a2.002 2.002 0 0 0 2-2v-6a2.002 2.002 0 0 0-2-2z" fill="currentColor"></path><circle cx="7" cy="25" r="1" fill="currentColor"></circle><path d="M22.707 7.293l-5-5A1 1 0 0 0 17 2h-6a2.002 2.002 0 0 0-2 2v16a2.002 2.002 0 0 0 2 2h10a2.002 2.002 0 0 0 2-2V8a1 1 0 0 0-.293-.707zM20.586 8H17V4.414zM11 20V4h4v4a2.002 2.002 0 0 0 2 2h4v10z" fill="currentColor"></path></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 557 B  | 
@@ -1,62 +1,113 @@
 | 
				
			|||||||
<script setup async lang="ts">
 | 
					<script setup async lang="ts">
 | 
				
			||||||
import { provide, ref } from "vue";
 | 
					import type { MenuOption } from 'naive-ui';
 | 
				
			||||||
import { useRouter } from "vue-router";
 | 
					import { provide, ref, h } from 'vue';
 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					import { useRouter, RouterLink } from 'vue-router';
 | 
				
			||||||
 | 
					import type { TokenInjectType } from '@/api';
 | 
				
			||||||
 | 
					import { useMessage, NMenu, NPageHeader, NIcon } from 'naive-ui';
 | 
				
			||||||
 | 
					import { BareMetalServer02 } from '@vicons/carbon';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = useRouter();
 | 
					const router = useRouter();
 | 
				
			||||||
 | 
					const message = useMessage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jwt = ref<string | null>(localStorage.getItem("token"));
 | 
					const jwt = ref<string | null>(localStorage.getItem('token'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setToken(token: string) {
 | 
					function setToken(token: string) {
 | 
				
			||||||
  jwt.value = token;
 | 
						jwt.value = token;
 | 
				
			||||||
  localStorage.setItem("token", token);
 | 
						localStorage.setItem('token', token);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function logout() {
 | 
					function logout() {
 | 
				
			||||||
  jwt.value = null;
 | 
						jwt.value = null;
 | 
				
			||||||
  localStorage.removeItem("token");
 | 
						localStorage.removeItem('token');
 | 
				
			||||||
  router.push({ name: "login" });
 | 
						router.push({ name: 'login' });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
provide<TokenInjectType>("jwt", {
 | 
					provide<TokenInjectType>('jwt', {
 | 
				
			||||||
  jwt,
 | 
						jwt,
 | 
				
			||||||
  setToken,
 | 
						setToken,
 | 
				
			||||||
  logout,
 | 
						logout
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.afterEach(() => message.destroyAll());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleUpdateValue(key: string) {
 | 
				
			||||||
 | 
						if (key === 'login') logout();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const menuOptions: MenuOption[] = [
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							label: () =>
 | 
				
			||||||
 | 
								h(
 | 
				
			||||||
 | 
									RouterLink,
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										to: '/'
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									{ default: () => 'Files' }
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							key: 'fs'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							label: () =>
 | 
				
			||||||
 | 
								h(
 | 
				
			||||||
 | 
									RouterLink,
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										to: '/profile'
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									{ default: () => 'Profile' }
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							key: 'profile'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							label: () =>
 | 
				
			||||||
 | 
								h(
 | 
				
			||||||
 | 
									RouterLink,
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										to: '/login'
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									{ default: () => 'Logout' }
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							key: 'login'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <nav>
 | 
						<n-page-header style="margin-bottom: 3em">
 | 
				
			||||||
    <template v-if="jwt != null">
 | 
							<template #title>
 | 
				
			||||||
      <router-link to="/">Files</router-link>
 | 
								<n-icon class="nav-icon" size="1.5em">
 | 
				
			||||||
      <span style="margin-left: 2em" />
 | 
									<BareMetalServer02 />
 | 
				
			||||||
      <router-link to="/profile">Profile</router-link>
 | 
								</n-icon>
 | 
				
			||||||
      <span style="margin-left: 2em" />
 | 
								MFileserver
 | 
				
			||||||
      <router-link to="/login" @click="logout()">Logout</router-link>
 | 
							</template>
 | 
				
			||||||
    </template>
 | 
							<template #extra>
 | 
				
			||||||
  </nav>
 | 
								<n-menu
 | 
				
			||||||
  <router-view />
 | 
									v-if="jwt != null"
 | 
				
			||||||
 | 
									mode="horizontal"
 | 
				
			||||||
 | 
									:options="menuOptions"
 | 
				
			||||||
 | 
									@update:value="handleUpdateValue"
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							</template>
 | 
				
			||||||
 | 
						</n-page-header>
 | 
				
			||||||
 | 
						<router-view />
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss">
 | 
					<style lang="scss">
 | 
				
			||||||
#app {
 | 
					body {
 | 
				
			||||||
  font-family: Avenir, Helvetica, Arial, sans-serif;
 | 
						height: 100%;
 | 
				
			||||||
  -webkit-font-smoothing: antialiased;
 | 
						padding: 2em;
 | 
				
			||||||
  -moz-osx-font-smoothing: grayscale;
 | 
						display: flex;
 | 
				
			||||||
  text-align: center;
 | 
						justify-content: center;
 | 
				
			||||||
  color: #2c3e50;
 | 
						align-content: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
nav {
 | 
					#app {
 | 
				
			||||||
  padding: 30px;
 | 
						font-family: Avenir, Helvetica, Arial, sans-serif;
 | 
				
			||||||
 | 
						-webkit-font-smoothing: antialiased;
 | 
				
			||||||
 | 
						-moz-osx-font-smoothing: grayscale;
 | 
				
			||||||
 | 
						color: #2c3e50;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  a {
 | 
					.nav-icon {
 | 
				
			||||||
    font-weight: bold;
 | 
						top: 0.25em;
 | 
				
			||||||
    color: #2c3e50;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &.router-link-exact-active {
 | 
					 | 
				
			||||||
      color: #42b983;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,17 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import App from "./App.vue";
 | 
					import App from './App.vue';
 | 
				
			||||||
 | 
					import { NSpin, NMessageProvider, NDialogProvider } from 'naive-ui';
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <Suspense>
 | 
						<Suspense>
 | 
				
			||||||
    <App></App>
 | 
							<n-message-provider :closable="true" :duration="5000">
 | 
				
			||||||
    <template #fallback>
 | 
								<n-dialog-provider>
 | 
				
			||||||
      <div>Loading...</div>
 | 
									<App />
 | 
				
			||||||
    </template>
 | 
								</n-dialog-provider>
 | 
				
			||||||
  </Suspense>
 | 
							</n-message-provider>
 | 
				
			||||||
 | 
							<template #fallback>
 | 
				
			||||||
 | 
								<div><n-spin size="small" />Loading...</div>
 | 
				
			||||||
 | 
							</template>
 | 
				
			||||||
 | 
						</Suspense>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,54 +1,55 @@
 | 
				
			|||||||
import { Requests, Responses, UserRole, get_token, post_token } from "./base";
 | 
					import type { Requests, Responses } from '@/dto';
 | 
				
			||||||
 | 
					import { UserRole, get_token, post_token } from './base';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const get_users = (token: string): Promise<Responses.Admin.GetUsers> =>
 | 
					export const get_users = (token: string): Promise<Responses.GetUsers> =>
 | 
				
			||||||
  get_token("/api/admin/users", token);
 | 
						get_token('/api/admin/users', token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const set_role = (
 | 
					export const set_role = (
 | 
				
			||||||
  user: number,
 | 
						user: number,
 | 
				
			||||||
  role: UserRole,
 | 
						role: UserRole,
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Admin.SetUserRole | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
  post_token<Requests.Admin.SetUserRole>(
 | 
						post_token<Requests.SetUserRole>(
 | 
				
			||||||
    "/api/admin/set_role",
 | 
							'/api/admin/set_role',
 | 
				
			||||||
    {
 | 
							{
 | 
				
			||||||
      user,
 | 
								user,
 | 
				
			||||||
      role,
 | 
								role
 | 
				
			||||||
    },
 | 
							},
 | 
				
			||||||
    token
 | 
							token
 | 
				
			||||||
  );
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const logout = (
 | 
					export const logout = (
 | 
				
			||||||
  user: number,
 | 
						user: number,
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Admin.LogoutAllUser | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
  post_token<Requests.Admin.LogoutAll>(
 | 
						post_token<Requests.Admin>(
 | 
				
			||||||
    "/api/admin/logout",
 | 
							'/api/admin/logout',
 | 
				
			||||||
    {
 | 
							{
 | 
				
			||||||
      user,
 | 
								user
 | 
				
			||||||
    },
 | 
							},
 | 
				
			||||||
    token
 | 
							token
 | 
				
			||||||
  );
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const delete_user = (
 | 
					export const delete_user = (
 | 
				
			||||||
  user: number,
 | 
						user: number,
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Admin.DeleteUser | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
  post_token<Requests.Admin.DeleteUser>(
 | 
						post_token<Requests.Admin>(
 | 
				
			||||||
    "/api/admin/delete",
 | 
							'/api/admin/delete',
 | 
				
			||||||
    {
 | 
							{
 | 
				
			||||||
      user,
 | 
								user
 | 
				
			||||||
    },
 | 
							},
 | 
				
			||||||
    token
 | 
							token
 | 
				
			||||||
  );
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const disable_tfa = (
 | 
					export const disable_tfa = (
 | 
				
			||||||
  user: number,
 | 
						user: number,
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Admin.DisableTfa | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
  post_token<Requests.Admin.DisableTfa>(
 | 
						post_token<Requests.Admin>(
 | 
				
			||||||
    "/api/admin/disable_2fa",
 | 
							'/api/admin/disable_2fa',
 | 
				
			||||||
    {
 | 
							{
 | 
				
			||||||
      user,
 | 
								user
 | 
				
			||||||
    },
 | 
							},
 | 
				
			||||||
    token
 | 
							token
 | 
				
			||||||
  );
 | 
						);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,93 +1,86 @@
 | 
				
			|||||||
import { Responses, Requests, post, post_token } from "./base";
 | 
					import type { Requests, Responses } from '@/dto';
 | 
				
			||||||
 | 
					import { post, post_token } from './base';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const auth_login = (
 | 
					export const auth_login = (
 | 
				
			||||||
  username: string,
 | 
						username: string,
 | 
				
			||||||
  password: string,
 | 
						password: string,
 | 
				
			||||||
  otp?: string
 | 
						otp?: string
 | 
				
			||||||
): Promise<
 | 
					): Promise<Responses.Login | Responses.Success | Responses.Error> =>
 | 
				
			||||||
  | Responses.Auth.LoginResponse
 | 
						post<Requests.Login>('/api/auth/login', {
 | 
				
			||||||
  | Responses.Auth.TfaRequiredResponse
 | 
							username: username,
 | 
				
			||||||
  | Responses.ErrorResponse
 | 
							password: password,
 | 
				
			||||||
> =>
 | 
							otp: otp
 | 
				
			||||||
  post<Requests.Auth.LoginRequest>("/api/auth/login", {
 | 
						});
 | 
				
			||||||
    username: username,
 | 
					 | 
				
			||||||
    password: password,
 | 
					 | 
				
			||||||
    otp: otp,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const auth_signup = (
 | 
					export const auth_signup = (
 | 
				
			||||||
  username: string,
 | 
						username: string,
 | 
				
			||||||
  password: string
 | 
						password: string
 | 
				
			||||||
): Promise<Responses.Auth.SignupResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
  post<Requests.Auth.SignUpRequest>("/api/auth/signup", {
 | 
						post<Requests.SignUp>('/api/auth/signup', {
 | 
				
			||||||
    username: username,
 | 
							username: username,
 | 
				
			||||||
    password: password,
 | 
							password: password
 | 
				
			||||||
  });
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const refresh_token = (
 | 
					export const refresh_token = (
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Auth.RefreshResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Login | Responses.Error> =>
 | 
				
			||||||
  post_token("/api/auth/refresh", {}, token);
 | 
						post_token('/api/auth/refresh', {}, token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const change_password = (
 | 
					export const change_password = (
 | 
				
			||||||
  oldPw: string,
 | 
						oldPw: string,
 | 
				
			||||||
  newPw: string,
 | 
						newPw: string,
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Auth.ChangePasswordResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
  post_token<Requests.Auth.ChangePasswordRequest>(
 | 
						post_token<Requests.ChangePassword>(
 | 
				
			||||||
    "/api/auth/change_password",
 | 
							'/api/auth/change_password',
 | 
				
			||||||
    {
 | 
							{
 | 
				
			||||||
      oldPassword: oldPw,
 | 
								oldPassword: oldPw,
 | 
				
			||||||
      newPassword: newPw,
 | 
								newPassword: newPw
 | 
				
			||||||
    },
 | 
							},
 | 
				
			||||||
    token
 | 
							token
 | 
				
			||||||
  );
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const logout_all = (
 | 
					export const logout_all = (
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Auth.LogoutAllResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
  post_token("/api/auth/logout_all", {}, token);
 | 
						post_token('/api/auth/logout_all', {}, token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function tfa_setup(
 | 
					export function tfa_setup(
 | 
				
			||||||
  mail: false,
 | 
						mail: false,
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Auth.RequestTotpTfaResponse | Responses.ErrorResponse>;
 | 
					): Promise<Responses.RequestsTotpTfa | Responses.Error>;
 | 
				
			||||||
export function tfa_setup(
 | 
					export function tfa_setup(
 | 
				
			||||||
  mail: true,
 | 
						mail: true,
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Auth.RequestEmailTfaResponse | Responses.ErrorResponse>;
 | 
					): Promise<Responses.Success | Responses.Error>;
 | 
				
			||||||
export function tfa_setup(
 | 
					export function tfa_setup(
 | 
				
			||||||
  mail: boolean,
 | 
						mail: boolean,
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<
 | 
					): Promise<Responses.Success | Responses.RequestsTotpTfa | Responses.Error> {
 | 
				
			||||||
  | Responses.Auth.RequestEmailTfaResponse
 | 
						return post_token<Requests.TfaSetup>(
 | 
				
			||||||
  | Responses.Auth.RequestTotpTfaResponse
 | 
							'/api/auth/2fa/setup',
 | 
				
			||||||
  | Responses.ErrorResponse
 | 
							{
 | 
				
			||||||
> {
 | 
								mail
 | 
				
			||||||
  return post_token<Requests.Auth.TfaSetup>(
 | 
							},
 | 
				
			||||||
    "/api/auth/2fa/setup",
 | 
							token
 | 
				
			||||||
    {
 | 
						);
 | 
				
			||||||
      mail,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    token
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const tfa_complete = (
 | 
					export const tfa_complete = (
 | 
				
			||||||
  mail: boolean,
 | 
						mail: boolean,
 | 
				
			||||||
  code: string,
 | 
						code: string,
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Auth.TfaCompletedResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
  post_token<Requests.Auth.TfaComplete>(
 | 
						post_token<Requests.TfaComplete>(
 | 
				
			||||||
    "/api/auth/2fa/complete",
 | 
							'/api/auth/2fa/complete',
 | 
				
			||||||
    {
 | 
							{
 | 
				
			||||||
      mail,
 | 
								mail,
 | 
				
			||||||
      code,
 | 
								code
 | 
				
			||||||
    },
 | 
							},
 | 
				
			||||||
    token
 | 
							token
 | 
				
			||||||
  );
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const tfa_disable = (
 | 
					export const tfa_disable = (
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.Auth.RemoveTfaResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
  post_token("/api/auth/2fa/disable", {}, token);
 | 
						post_token('/api/auth/2fa/disable', {}, token);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,62 +1,62 @@
 | 
				
			|||||||
import axios from "axios";
 | 
					import axios from 'axios';
 | 
				
			||||||
import { Requests, Responses, UserRole } from "../dto";
 | 
					import type { Requests, Responses, UploadFile } from '@/dto';
 | 
				
			||||||
export { Requests, Responses, UserRole };
 | 
					import { UserRole } from '@/dto';
 | 
				
			||||||
 | 
					export { Requests, Responses, UserRole, UploadFile };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const post = <T extends Requests.BaseRequest>(url: string, data: T) =>
 | 
					export const post = <T extends Requests.Base>(url: string, data: T) =>
 | 
				
			||||||
  axios
 | 
						axios
 | 
				
			||||||
    .post(url, data, {
 | 
							.post(url, data, {
 | 
				
			||||||
      headers: { "Content-type": "application/json" },
 | 
								headers: { 'Content-type': 'application/json' }
 | 
				
			||||||
    })
 | 
							})
 | 
				
			||||||
    .then((res) => res.data)
 | 
							.then((res) => res.data)
 | 
				
			||||||
    .catch((err) => err.response.data);
 | 
							.catch((err) => err.response.data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const post_token = <T extends Requests.BaseRequest>(
 | 
					export const post_token = <T extends Requests.Base>(
 | 
				
			||||||
  url: string,
 | 
						url: string,
 | 
				
			||||||
  data: T,
 | 
						data: T,
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
) =>
 | 
					) =>
 | 
				
			||||||
  axios
 | 
						axios
 | 
				
			||||||
    .post(url, data, {
 | 
							.post(url, data, {
 | 
				
			||||||
      headers: {
 | 
								headers: {
 | 
				
			||||||
        Authorization: "Bearer " + token,
 | 
									Authorization: 'Bearer ' + token,
 | 
				
			||||||
        "Content-type": "application/json",
 | 
									'Content-type': 'application/json'
 | 
				
			||||||
      },
 | 
								}
 | 
				
			||||||
    })
 | 
							})
 | 
				
			||||||
    .then((res) => res.data)
 | 
							.then((res) => res.data)
 | 
				
			||||||
    .catch((err) => err.response.data);
 | 
							.catch((err) => err.response.data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const post_token_form = (
 | 
					export const post_token_form = (
 | 
				
			||||||
  url: string,
 | 
						url: string,
 | 
				
			||||||
  data: FormData,
 | 
						data: FormData,
 | 
				
			||||||
  token: string,
 | 
						token: string,
 | 
				
			||||||
  onProgress: (progressEvent: ProgressEvent) => void
 | 
						onProgress: (progressEvent: ProgressEvent) => void
 | 
				
			||||||
) =>
 | 
					) =>
 | 
				
			||||||
  axios
 | 
						axios
 | 
				
			||||||
    .post(url, data, {
 | 
							.post(url, data, {
 | 
				
			||||||
      headers: {
 | 
								headers: {
 | 
				
			||||||
        Authorization: "Bearer " + token,
 | 
									Authorization: 'Bearer ' + token,
 | 
				
			||||||
        "Content-type": "multipart/form-data",
 | 
									'Content-type': 'multipart/form-data'
 | 
				
			||||||
      },
 | 
								},
 | 
				
			||||||
      onUploadProgress: onProgress,
 | 
								onUploadProgress: onProgress
 | 
				
			||||||
    })
 | 
							})
 | 
				
			||||||
    .then((res) => res.data)
 | 
							.then((res) => res.data)
 | 
				
			||||||
    .catch((err) => err.response.data);
 | 
							.catch((err) => err.response.data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
export const get = (url: string) =>
 | 
					export const get = (url: string) =>
 | 
				
			||||||
  axios
 | 
						axios
 | 
				
			||||||
    .get(url)
 | 
							.get(url)
 | 
				
			||||||
    .then((res) => res.data)
 | 
							.then((res) => res.data)
 | 
				
			||||||
    .catch((err) => err.response.data);
 | 
							.catch((err) => err.response.data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const get_token = (url: string, token: string) =>
 | 
					export const get_token = (url: string, token: string) =>
 | 
				
			||||||
  axios
 | 
						axios
 | 
				
			||||||
    .get(url, {
 | 
							.get(url, {
 | 
				
			||||||
      headers: { Authorization: "Bearer " + token },
 | 
								headers: { Authorization: 'Bearer ' + token }
 | 
				
			||||||
    })
 | 
							})
 | 
				
			||||||
    .then((res) => res.data)
 | 
							.then((res) => res.data)
 | 
				
			||||||
    .catch((err) => err.response.data);
 | 
							.catch((err) => err.response.data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const isErrorResponse = (
 | 
					export const isErrorResponse = (res: Responses.Base): res is Responses.Error =>
 | 
				
			||||||
  res: Responses.BaseResponse
 | 
						res.statusCode > 299;
 | 
				
			||||||
): res is Responses.ErrorResponse => res.statusCode != 200;
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,84 +1,124 @@
 | 
				
			|||||||
 | 
					import type { Requests, Responses, UploadFile } from '@/dto';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Responses,
 | 
						get_token,
 | 
				
			||||||
  Requests,
 | 
						post_token,
 | 
				
			||||||
  get_token,
 | 
						post_token_form,
 | 
				
			||||||
  post_token,
 | 
						isErrorResponse
 | 
				
			||||||
  post_token_form,
 | 
					} from './base';
 | 
				
			||||||
  isErrorResponse,
 | 
					 | 
				
			||||||
} from "./base";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const get_root = (
 | 
					export const get_root = (
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.FS.GetRootResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.GetRoot | Responses.Error> =>
 | 
				
			||||||
  get_token("/api/fs/root", token);
 | 
						get_token('/api/fs/root', token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const get_node = (
 | 
					export const get_node = (
 | 
				
			||||||
  token: string,
 | 
						token: string,
 | 
				
			||||||
  node: number
 | 
						node: number
 | 
				
			||||||
): Promise<Responses.FS.GetNodeResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.GetNode | Responses.Error> =>
 | 
				
			||||||
  get_token(`/api/fs/node/${node}`, token);
 | 
						get_token(`/api/fs/node/${node}`, token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const get_path = (
 | 
					export const get_path = (
 | 
				
			||||||
  token: string,
 | 
						token: string,
 | 
				
			||||||
  node: number
 | 
						node: number
 | 
				
			||||||
): Promise<Responses.FS.GetPathResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.GetPath | Responses.Error> =>
 | 
				
			||||||
  get_token(`/api/fs/path/${node}`, token);
 | 
						get_token(`/api/fs/path/${node}`, token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const create_folder = (
 | 
					export const create_folder = (
 | 
				
			||||||
  token: string,
 | 
						token: string,
 | 
				
			||||||
  parent: number,
 | 
						parent: number,
 | 
				
			||||||
  name: string
 | 
						name: string
 | 
				
			||||||
): Promise<Responses.FS.CreateFolderResponse | Responses.ErrorResponse> =>
 | 
					): Promise<
 | 
				
			||||||
  post_token<Requests.FS.CreateFolderRequest>(
 | 
						Responses.CreateFolder | Responses.CreateFolderExists | Responses.Error
 | 
				
			||||||
    "/api/fs/createFolder",
 | 
					> =>
 | 
				
			||||||
    {
 | 
						post_token<Requests.CreateFolder>(
 | 
				
			||||||
      parent: parent,
 | 
							'/api/fs/createFolder',
 | 
				
			||||||
      name: name,
 | 
							{
 | 
				
			||||||
    },
 | 
								parent: parent,
 | 
				
			||||||
    token
 | 
								name: name
 | 
				
			||||||
  );
 | 
							},
 | 
				
			||||||
 | 
							token
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const create_file = (
 | 
					export const create_file = (
 | 
				
			||||||
  token: string,
 | 
						token: string,
 | 
				
			||||||
  parent: number,
 | 
						parent: number,
 | 
				
			||||||
  name: string
 | 
						name: string
 | 
				
			||||||
): Promise<Responses.FS.CreateFileResponse | Responses.ErrorResponse> =>
 | 
					): Promise<
 | 
				
			||||||
  post_token<Requests.FS.CreateFileRequest>(
 | 
						Responses.CreateFolder | Responses.CreateFolderExists | Responses.Error
 | 
				
			||||||
    "/api/fs/createFile",
 | 
					> =>
 | 
				
			||||||
    {
 | 
						post_token<Requests.CreateFolder>(
 | 
				
			||||||
      parent: parent,
 | 
							'/api/fs/createFile',
 | 
				
			||||||
      name: name,
 | 
							{
 | 
				
			||||||
    },
 | 
								parent: parent,
 | 
				
			||||||
    token
 | 
								name: name
 | 
				
			||||||
  );
 | 
							},
 | 
				
			||||||
 | 
							token
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const delete_node = (
 | 
					export const create_zip = (
 | 
				
			||||||
  token: string,
 | 
						token: string,
 | 
				
			||||||
  node: number
 | 
						nodes: number[]
 | 
				
			||||||
): Promise<Responses.FS.DeleteResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.CreateZip | Responses.Error> =>
 | 
				
			||||||
  post_token(`/api/fs/delete/${node}`, {}, token);
 | 
						post_token<Requests.CreateZip>(
 | 
				
			||||||
 | 
							'/api/fs/create_zip',
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								nodes: nodes
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							token
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const upload_file = async (
 | 
					export const download_preview = (
 | 
				
			||||||
  token: string,
 | 
						token: string,
 | 
				
			||||||
  parent: number,
 | 
						node: number
 | 
				
			||||||
  file: File,
 | 
					): Promise<Responses.DownloadBase64 | Responses.Error> =>
 | 
				
			||||||
  onProgress: (progressEvent: ProgressEvent) => void
 | 
						get_token(`/api/fs/download_preview/${node}`, token);
 | 
				
			||||||
): Promise<Responses.FS.UploadFileResponse | Responses.ErrorResponse> => {
 | 
					 | 
				
			||||||
  const node = await create_file(token, parent, file.name);
 | 
					 | 
				
			||||||
  if (isErrorResponse(node)) return node;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const form = new FormData();
 | 
					export const get_type = (
 | 
				
			||||||
  form.set("file", file);
 | 
						token: string,
 | 
				
			||||||
  return post_token_form(`/api/fs/upload/${node.id}`, form, token, onProgress);
 | 
						node: number
 | 
				
			||||||
};
 | 
					): Promise<Responses.GetType | Responses.Error> =>
 | 
				
			||||||
 | 
						get_token(`/api/fs/get_type/${node}`, token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function upload_file(
 | 
				
			||||||
 | 
						token: string,
 | 
				
			||||||
 | 
						file: UploadFile,
 | 
				
			||||||
 | 
						onProgress: (progressEvent: ProgressEvent) => void
 | 
				
			||||||
 | 
					): Promise<Responses.Success | Responses.Error> {
 | 
				
			||||||
 | 
						const node = await create_file(token, file.parent, file.file.name);
 | 
				
			||||||
 | 
						if (isErrorResponse(node)) return node;
 | 
				
			||||||
 | 
						if ('exists' in node && !node.isFile)
 | 
				
			||||||
 | 
							return { statusCode: 400, message: 'File exists as folder' };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const form = new FormData();
 | 
				
			||||||
 | 
						form.set('file', file.file);
 | 
				
			||||||
 | 
						return post_token_form(
 | 
				
			||||||
 | 
							`/api/fs/upload/${node.id}`,
 | 
				
			||||||
 | 
							form,
 | 
				
			||||||
 | 
							token,
 | 
				
			||||||
 | 
							onProgress
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function download_file(token: string, id: number) {
 | 
					export function download_file(token: string, id: number) {
 | 
				
			||||||
  const form = document.createElement("form");
 | 
						const form = document.createElement('form');
 | 
				
			||||||
  form.method = "post";
 | 
						form.method = 'post';
 | 
				
			||||||
  form.target = "_blank";
 | 
						form.target = '_blank';
 | 
				
			||||||
  form.action = "/api/fs/download";
 | 
						form.action = '/api/fs/download';
 | 
				
			||||||
  form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${id}">`;
 | 
						form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${id}">`;
 | 
				
			||||||
  document.body.appendChild(form);
 | 
						document.body.appendChild(form);
 | 
				
			||||||
  form.submit();
 | 
						form.submit();
 | 
				
			||||||
  document.body.removeChild(form);
 | 
						document.body.removeChild(form);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function download_multi_file(token: string, ids: number[]) {
 | 
				
			||||||
 | 
						const form = document.createElement('form');
 | 
				
			||||||
 | 
						form.method = 'post';
 | 
				
			||||||
 | 
						form.target = '_blank';
 | 
				
			||||||
 | 
						form.action = '/api/fs/download_multi';
 | 
				
			||||||
 | 
						form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${ids.join(
 | 
				
			||||||
 | 
							','
 | 
				
			||||||
 | 
						)}">`;
 | 
				
			||||||
 | 
						document.body.appendChild(form);
 | 
				
			||||||
 | 
						form.submit();
 | 
				
			||||||
 | 
						document.body.removeChild(form);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
export { Requests, Responses, UserRole, isErrorResponse } from "./base";
 | 
					export type { Requests, Responses, UploadFile } from './base';
 | 
				
			||||||
export * as Auth from "./auth";
 | 
					export { UserRole, isErrorResponse } from './base';
 | 
				
			||||||
export * as FS from "./fs";
 | 
					export * as Auth from './auth';
 | 
				
			||||||
export * as User from "./user";
 | 
					export * as FS from './fs';
 | 
				
			||||||
export * as Admin from "./admin";
 | 
					export * as User from './user';
 | 
				
			||||||
export * from "./util";
 | 
					export * as Admin from './admin';
 | 
				
			||||||
 | 
					export * from './util';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,12 @@
 | 
				
			|||||||
import { Responses, get_token, post_token } from "@/api/base";
 | 
					import type { Responses } from '@/api/base';
 | 
				
			||||||
 | 
					import { get_token, post_token } from '@/api/base';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const get_user_info = (
 | 
					export const get_user_info = (
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.User.UserInfoResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.UserInfo | Responses.Error> =>
 | 
				
			||||||
  get_token("/api/user/info", token);
 | 
						get_token('/api/user/info', token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const delete_user = (
 | 
					export const delete_user = (
 | 
				
			||||||
  token: string
 | 
						token: string
 | 
				
			||||||
): Promise<Responses.User.DeleteUserResponse | Responses.ErrorResponse> =>
 | 
					): Promise<Responses.Success | Responses.Error> =>
 | 
				
			||||||
  post_token("/api/user/delete", {}, token);
 | 
						post_token('/api/user/delete', {}, token);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,26 +1,26 @@
 | 
				
			|||||||
import type { JwtPayload } from "jwt-decode";
 | 
					import type { JwtPayload } from 'jwt-decode';
 | 
				
			||||||
import type { Ref, UnwrapRef } from "vue";
 | 
					import type { Ref, UnwrapRef } from 'vue';
 | 
				
			||||||
import jwtDecode from "jwt-decode";
 | 
					import jwtDecode from 'jwt-decode';
 | 
				
			||||||
import { isErrorResponse } from "./base";
 | 
					import { isErrorResponse } from './base';
 | 
				
			||||||
import { refresh_token } from "./auth";
 | 
					import { refresh_token } from './auth';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function check_token(
 | 
					export async function check_token(
 | 
				
			||||||
  token: TokenInjectType
 | 
						token: TokenInjectType
 | 
				
			||||||
): Promise<string | void> {
 | 
					): Promise<string | void> {
 | 
				
			||||||
  if (!token.jwt.value) return token.logout();
 | 
						if (!token.jwt.value) return token.logout();
 | 
				
			||||||
  const payload = jwtDecode<JwtPayload>(token.jwt.value);
 | 
						const payload = jwtDecode<JwtPayload>(token.jwt.value);
 | 
				
			||||||
  if (!payload) return token.logout();
 | 
						if (!payload) return token.logout();
 | 
				
			||||||
  // Expires in more than 60 Minute
 | 
						// Expires in more than 60 Minute
 | 
				
			||||||
  if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60))
 | 
						if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60))
 | 
				
			||||||
    return token.jwt.value;
 | 
							return token.jwt.value;
 | 
				
			||||||
  const new_token = await refresh_token(token.jwt.value);
 | 
						const new_token = await refresh_token(token.jwt.value);
 | 
				
			||||||
  if (isErrorResponse(new_token)) return token.logout();
 | 
						if (isErrorResponse(new_token)) return token.logout();
 | 
				
			||||||
  token.setToken(new_token.jwt);
 | 
						token.setToken(new_token.jwt);
 | 
				
			||||||
  return new_token.jwt;
 | 
						return new_token.jwt;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type TokenInjectType = {
 | 
					export type TokenInjectType = {
 | 
				
			||||||
  jwt: Ref<UnwrapRef<string | null>>;
 | 
						jwt: Ref<UnwrapRef<string | null>>;
 | 
				
			||||||
  setToken: (token: string) => void;
 | 
						setToken: (token: string) => void;
 | 
				
			||||||
  logout: () => void;
 | 
						logout: () => void;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +0,0 @@
 | 
				
			|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"  xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 308 B  | 
							
								
								
									
										31
									
								
								frontend/src/components/AsyncImage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								frontend/src/components/AsyncImage.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					<script setup async lang="ts">
 | 
				
			||||||
 | 
					import type { TokenInjectType } from '@/api';
 | 
				
			||||||
 | 
					import { inject, ref } from 'vue';
 | 
				
			||||||
 | 
					import { NImage } from 'naive-ui';
 | 
				
			||||||
 | 
					import { check_token, FS, isErrorResponse } from '@/api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
						alt: string;
 | 
				
			||||||
 | 
						id: number;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const success = ref(false);
 | 
				
			||||||
 | 
					const data = ref('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const token = await check_token(jwt);
 | 
				
			||||||
 | 
					if (token) {
 | 
				
			||||||
 | 
						const resp = await FS.download_preview(jwt.jwt.value ?? '', props.id);
 | 
				
			||||||
 | 
						if (!isErrorResponse(resp)) {
 | 
				
			||||||
 | 
							data.value = resp.data;
 | 
				
			||||||
 | 
							success.value = true;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<NImage v-if="success" :alt="alt" :src="data" />
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss"></style>
 | 
				
			||||||
							
								
								
									
										90
									
								
								frontend/src/components/DirViewer/CreateZipDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								frontend/src/components/DirViewer/CreateZipDialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					import type { TokenInjectType } from '@/api';
 | 
				
			||||||
 | 
					import { ref } from 'vue';
 | 
				
			||||||
 | 
					import { NProgress, NButton, NIcon } from 'naive-ui';
 | 
				
			||||||
 | 
					import filesize from 'filesize';
 | 
				
			||||||
 | 
					import { Archive, Download } from '@vicons/carbon';
 | 
				
			||||||
 | 
					import { FS, check_token, isErrorResponse } from '@/api';
 | 
				
			||||||
 | 
					import type { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function createZipDialog(
 | 
				
			||||||
 | 
						nodes: number[],
 | 
				
			||||||
 | 
						dialog: DialogApiInjection,
 | 
				
			||||||
 | 
						jwt: TokenInjectType
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						const progress = ref(0);
 | 
				
			||||||
 | 
						const total = ref(1);
 | 
				
			||||||
 | 
						const percentage = ref(0);
 | 
				
			||||||
 | 
						const done = ref(false);
 | 
				
			||||||
 | 
						const dia = dialog.create({
 | 
				
			||||||
 | 
							title: 'Create Archive...',
 | 
				
			||||||
 | 
							closable: false,
 | 
				
			||||||
 | 
							closeOnEsc: false,
 | 
				
			||||||
 | 
							maskClosable: false,
 | 
				
			||||||
 | 
							icon: () => <Archive />,
 | 
				
			||||||
 | 
							content: () => (
 | 
				
			||||||
 | 
								<NProgress
 | 
				
			||||||
 | 
									type="line"
 | 
				
			||||||
 | 
									percentage={percentage.value}
 | 
				
			||||||
 | 
									height={20}
 | 
				
			||||||
 | 
									status="info"
 | 
				
			||||||
 | 
									showIndicator={false}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
							action: () =>
 | 
				
			||||||
 | 
								done.value ? (
 | 
				
			||||||
 | 
									<NButton
 | 
				
			||||||
 | 
										onClick={async () => {
 | 
				
			||||||
 | 
											const token = await check_token(jwt);
 | 
				
			||||||
 | 
											if (!token) return;
 | 
				
			||||||
 | 
											if (nodes.length == 1)
 | 
				
			||||||
 | 
												FS.download_file(token, nodes[0]);
 | 
				
			||||||
 | 
											else FS.download_multi_file(token, nodes);
 | 
				
			||||||
 | 
											dia.destroy();
 | 
				
			||||||
 | 
										}}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										{{
 | 
				
			||||||
 | 
											icon: () => (
 | 
				
			||||||
 | 
												<NIcon>
 | 
				
			||||||
 | 
													<Download />
 | 
				
			||||||
 | 
												</NIcon>
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											default: () => 'Download archive'
 | 
				
			||||||
 | 
										}}
 | 
				
			||||||
 | 
									</NButton>
 | 
				
			||||||
 | 
								) : (
 | 
				
			||||||
 | 
									<div>
 | 
				
			||||||
 | 
										{filesize(progress.value, {
 | 
				
			||||||
 | 
											base: 2,
 | 
				
			||||||
 | 
											standard: 'jedec'
 | 
				
			||||||
 | 
										})}
 | 
				
			||||||
 | 
										/
 | 
				
			||||||
 | 
										{filesize(total.value, {
 | 
				
			||||||
 | 
											base: 2,
 | 
				
			||||||
 | 
											standard: 'jedec'
 | 
				
			||||||
 | 
										})}
 | 
				
			||||||
 | 
										- {Math.floor(percentage.value * 1000) / 1000}%
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						let updateRunning = false;
 | 
				
			||||||
 | 
						const updateInterval = setInterval(async () => {
 | 
				
			||||||
 | 
							if (updateRunning) return;
 | 
				
			||||||
 | 
							updateRunning = true;
 | 
				
			||||||
 | 
							const token = await check_token(jwt);
 | 
				
			||||||
 | 
							if (!token) return;
 | 
				
			||||||
 | 
							const resp = await FS.create_zip(token, nodes);
 | 
				
			||||||
 | 
							if (isErrorResponse(resp)) return;
 | 
				
			||||||
 | 
							if (resp.done) {
 | 
				
			||||||
 | 
								percentage.value = 100;
 | 
				
			||||||
 | 
								clearInterval(updateInterval);
 | 
				
			||||||
 | 
								done.value = true;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								progress.value = resp.progress ?? 0;
 | 
				
			||||||
 | 
								total.value = resp.total ?? 1;
 | 
				
			||||||
 | 
								if (total.value == 0) total.value = 1;
 | 
				
			||||||
 | 
								percentage.value = (progress.value / total.value) * 100;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							updateRunning = false;
 | 
				
			||||||
 | 
						}, 500);
 | 
				
			||||||
 | 
						return dia;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										74
									
								
								frontend/src/components/DirViewer/DeleteModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								frontend/src/components/DirViewer/DeleteModal.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { TokenInjectType } from '@/api';
 | 
				
			||||||
 | 
					import type { LogInst } from 'naive-ui';
 | 
				
			||||||
 | 
					import { ref, inject } from 'vue';
 | 
				
			||||||
 | 
					import { check_token } from '@/api';
 | 
				
			||||||
 | 
					import { NCard, NLog } from 'naive-ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					const log = ref('');
 | 
				
			||||||
 | 
					const logInst = ref<LogInst>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getLogWriter() {
 | 
				
			||||||
 | 
						const decoder = new TextDecoder();
 | 
				
			||||||
 | 
						return new WritableStream<Uint8Array>({
 | 
				
			||||||
 | 
							write(chunk) {
 | 
				
			||||||
 | 
								log.value += decoder.decode(chunk, { stream: true });
 | 
				
			||||||
 | 
								logInst.value?.scrollTo({ position: 'top' });
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							close() {
 | 
				
			||||||
 | 
								log.value += decoder.decode(new Uint8Array(0), { stream: false });
 | 
				
			||||||
 | 
								logInst.value?.scrollTo({ position: 'top' });
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							abort(err) {
 | 
				
			||||||
 | 
								log.value += `Error: ${err}\n`;
 | 
				
			||||||
 | 
								logInst.value?.scrollTo({ position: 'top' });
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
						nodes: number[];
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function startDelete() {
 | 
				
			||||||
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
 | 
						if (!token) return;
 | 
				
			||||||
 | 
						for (const node of props.nodes) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const logWriter = getLogWriter();
 | 
				
			||||||
 | 
								const resp = await fetch(`/api/fs/delete/${node}`, {
 | 
				
			||||||
 | 
									method: 'post',
 | 
				
			||||||
 | 
									headers: {
 | 
				
			||||||
 | 
										Authorization: 'Bearer ' + token
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								if (!resp.ok) continue;
 | 
				
			||||||
 | 
								if (!resp.body) continue;
 | 
				
			||||||
 | 
								await resp.body.pipeTo(logWriter);
 | 
				
			||||||
 | 
							} catch (err) {
 | 
				
			||||||
 | 
								log.value += `Error: ${err}\n`;
 | 
				
			||||||
 | 
								logInst.value?.scrollTo({ position: 'top' });
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineExpose({
 | 
				
			||||||
 | 
						startDelete
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<n-card title="Deleting..." style="margin: 20px">
 | 
				
			||||||
 | 
							<n-log ref="logInst" class="log-code" :log="log" :rows="50"></n-log>
 | 
				
			||||||
 | 
							<!--<n-code class="log-code">
 | 
				
			||||||
 | 
							</n-code>-->
 | 
				
			||||||
 | 
						</n-card>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped lang="scss">
 | 
				
			||||||
 | 
					.log-code {
 | 
				
			||||||
 | 
						margin: 8px;
 | 
				
			||||||
 | 
						background-color: rgb(250, 250, 252);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										124
									
								
								frontend/src/components/DirViewer/DirViewer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								frontend/src/components/DirViewer/DirViewer.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
				
			|||||||
 | 
					<script setup lang="tsx">
 | 
				
			||||||
 | 
					import type { TokenInjectType, Responses } from '@/api';
 | 
				
			||||||
 | 
					import type { CSSProperties } from 'vue';
 | 
				
			||||||
 | 
					import { inject, ref, watch } from 'vue';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						useMessage,
 | 
				
			||||||
 | 
						useDialog,
 | 
				
			||||||
 | 
						NSwitch,
 | 
				
			||||||
 | 
						NGrid,
 | 
				
			||||||
 | 
						NGi,
 | 
				
			||||||
 | 
						NButton,
 | 
				
			||||||
 | 
						NIcon,
 | 
				
			||||||
 | 
						NInput
 | 
				
			||||||
 | 
					} from 'naive-ui';
 | 
				
			||||||
 | 
					import { FolderAdd } from '@vicons/carbon';
 | 
				
			||||||
 | 
					import { FS, check_token } from '@/api';
 | 
				
			||||||
 | 
					import DirViewerTable from '@/components/DirViewer/DirViewerTable.vue';
 | 
				
			||||||
 | 
					import { loadingMsgWrapper } from '@/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const message = useMessage();
 | 
				
			||||||
 | 
					const dialog = useDialog();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
						node: Responses.GetNode;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<{
 | 
				
			||||||
 | 
						(e: 'reloadNode'): void;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showPreview = ref(false);
 | 
				
			||||||
 | 
					const nodes = ref<Responses.GetNodeEntry[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
						() => props.node,
 | 
				
			||||||
 | 
						async (to) => {
 | 
				
			||||||
 | 
							nodes.value = [];
 | 
				
			||||||
 | 
							if (to.parent != null)
 | 
				
			||||||
 | 
								nodes.value.push({
 | 
				
			||||||
 | 
									id: to.parent,
 | 
				
			||||||
 | 
									isFile: false,
 | 
				
			||||||
 | 
									parent: null,
 | 
				
			||||||
 | 
									name: '..',
 | 
				
			||||||
 | 
									preview: false
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							if (to.children) nodes.value.push(...to.children);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{ immediate: true }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const newFolder = loadingMsgWrapper(message, async (name: string) => {
 | 
				
			||||||
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
 | 
						if (!token) return;
 | 
				
			||||||
 | 
						await FS.create_folder(token, props.node.id, name);
 | 
				
			||||||
 | 
						emit('reloadNode');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function previewSwitchRailStyle(state: { focused: boolean; checked: boolean }) {
 | 
				
			||||||
 | 
						const style: CSSProperties = {};
 | 
				
			||||||
 | 
						style.background = state.checked ? '#0b0' : '#d00';
 | 
				
			||||||
 | 
						if (state.focused)
 | 
				
			||||||
 | 
							style.boxShadow = `0 0 0 2px ${
 | 
				
			||||||
 | 
								state.checked ? '#00bb0040' : '#dd000040'
 | 
				
			||||||
 | 
							}`;
 | 
				
			||||||
 | 
						return style;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createNewFolderDialog() {
 | 
				
			||||||
 | 
						let newFolderName = '';
 | 
				
			||||||
 | 
						const dia = dialog.create({
 | 
				
			||||||
 | 
							title: 'New Folder',
 | 
				
			||||||
 | 
							icon: () => <FolderAdd />,
 | 
				
			||||||
 | 
							content: () => (
 | 
				
			||||||
 | 
								<NInput
 | 
				
			||||||
 | 
									type="text"
 | 
				
			||||||
 | 
									placeholder="Folder name"
 | 
				
			||||||
 | 
									onInput={(e) => (newFolderName = e)}
 | 
				
			||||||
 | 
									onKeyup={(e) => {
 | 
				
			||||||
 | 
										if (e.key === 'Enter')
 | 
				
			||||||
 | 
											newFolder(newFolderName).then(() => dia.destroy());
 | 
				
			||||||
 | 
									}}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
							negativeText: 'Cancel',
 | 
				
			||||||
 | 
							positiveText: 'Create',
 | 
				
			||||||
 | 
							positiveButtonProps: { type: 'success' },
 | 
				
			||||||
 | 
							onPositiveClick: () => newFolder(newFolderName)
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						return dia;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<n-grid cols="2" x-gap="16" y-gap="16">
 | 
				
			||||||
 | 
							<n-gi>
 | 
				
			||||||
 | 
								<n-button @click="createNewFolderDialog">
 | 
				
			||||||
 | 
									<template #icon>
 | 
				
			||||||
 | 
										<n-icon><FolderAdd /></n-icon>
 | 
				
			||||||
 | 
									</template>
 | 
				
			||||||
 | 
									Create folder
 | 
				
			||||||
 | 
								</n-button>
 | 
				
			||||||
 | 
							</n-gi>
 | 
				
			||||||
 | 
							<n-gi style="text-align: right">
 | 
				
			||||||
 | 
								<n-switch
 | 
				
			||||||
 | 
									:rail-style="previewSwitchRailStyle"
 | 
				
			||||||
 | 
									v-model:value="showPreview"
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									<template #checked>Show preview</template>
 | 
				
			||||||
 | 
									<template #unchecked>Hide preview</template>
 | 
				
			||||||
 | 
								</n-switch>
 | 
				
			||||||
 | 
							</n-gi>
 | 
				
			||||||
 | 
							<n-gi span="2">
 | 
				
			||||||
 | 
								<DirViewerTable
 | 
				
			||||||
 | 
									:nodes="nodes"
 | 
				
			||||||
 | 
									:show-preview="showPreview"
 | 
				
			||||||
 | 
									@reloadNode="emit('reloadNode')"
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							</n-gi>
 | 
				
			||||||
 | 
						</n-grid>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped></style>
 | 
				
			||||||
							
								
								
									
										379
									
								
								frontend/src/components/DirViewer/DirViewerTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								frontend/src/components/DirViewer/DirViewerTable.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,379 @@
 | 
				
			|||||||
 | 
					<script setup lang="tsx">
 | 
				
			||||||
 | 
					import type { TokenInjectType, Responses } from '@/api';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
						DropdownOption,
 | 
				
			||||||
 | 
						DropdownGroupOption,
 | 
				
			||||||
 | 
						DropdownDividerOption,
 | 
				
			||||||
 | 
						DropdownRenderOption,
 | 
				
			||||||
 | 
						DataTableColumn
 | 
				
			||||||
 | 
					} from 'naive-ui';
 | 
				
			||||||
 | 
					import type { SummaryCell } from 'naive-ui/es/data-table/src/interface';
 | 
				
			||||||
 | 
					import { inject, ref, nextTick, Suspense } from 'vue';
 | 
				
			||||||
 | 
					import filesize from 'filesize';
 | 
				
			||||||
 | 
					import { check_token, FS } from '@/api';
 | 
				
			||||||
 | 
					import { loadingMsgWrapper } from '@/utils';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						useMessage,
 | 
				
			||||||
 | 
						useDialog,
 | 
				
			||||||
 | 
						NDataTable,
 | 
				
			||||||
 | 
						NText,
 | 
				
			||||||
 | 
						NIcon,
 | 
				
			||||||
 | 
						NDropdown,
 | 
				
			||||||
 | 
						NPopover,
 | 
				
			||||||
 | 
						NSpin,
 | 
				
			||||||
 | 
						NImageGroup,
 | 
				
			||||||
 | 
						NButtonGroup,
 | 
				
			||||||
 | 
						NButton,
 | 
				
			||||||
 | 
						NModal
 | 
				
			||||||
 | 
					} from 'naive-ui';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						Folder,
 | 
				
			||||||
 | 
						FolderParent,
 | 
				
			||||||
 | 
						DocumentBlank,
 | 
				
			||||||
 | 
						Delete,
 | 
				
			||||||
 | 
						Download
 | 
				
			||||||
 | 
					} from '@vicons/carbon';
 | 
				
			||||||
 | 
					import NLink from '@/components/NLink.vue';
 | 
				
			||||||
 | 
					import AsyncImage from '@/components/AsyncImage.vue';
 | 
				
			||||||
 | 
					import createZipDialog from '@/components/DirViewer/CreateZipDialog';
 | 
				
			||||||
 | 
					import DeleteModal from '@/components/DirViewer/DeleteModal.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const message = useMessage();
 | 
				
			||||||
 | 
					const dialog = useDialog();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<{
 | 
				
			||||||
 | 
						(e: 'reloadNode'): void;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type DropdownOptionsType = Array<
 | 
				
			||||||
 | 
						| DropdownOption
 | 
				
			||||||
 | 
						| DropdownGroupOption
 | 
				
			||||||
 | 
						| DropdownDividerOption
 | 
				
			||||||
 | 
						| DropdownRenderOption
 | 
				
			||||||
 | 
					>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
						nodes: Responses.GetNodeEntry[];
 | 
				
			||||||
 | 
						showPreview: boolean;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const checkedRows = ref<number[]>([]);
 | 
				
			||||||
 | 
					const deleteNodes = ref<number[]>([]);
 | 
				
			||||||
 | 
					const deleteDialog = ref();
 | 
				
			||||||
 | 
					const deleteDialogShow = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dropdownX = ref(0);
 | 
				
			||||||
 | 
					const dropdownY = ref(0);
 | 
				
			||||||
 | 
					const dropdownShow = ref(false);
 | 
				
			||||||
 | 
					let dropdownCurrentNode: Responses.GetNodeEntry | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dropdownOptions = ref<DropdownOptionsType>();
 | 
				
			||||||
 | 
					const dropdownOptionsFolder: DropdownOptionsType = [
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							label: () => <NText>Download</NText>,
 | 
				
			||||||
 | 
							key: 'download',
 | 
				
			||||||
 | 
							icon: () => (
 | 
				
			||||||
 | 
								<NIcon>
 | 
				
			||||||
 | 
									<Download />
 | 
				
			||||||
 | 
								</NIcon>
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							label: () => <NText type="error">Delete</NText>,
 | 
				
			||||||
 | 
							key: 'delete',
 | 
				
			||||||
 | 
							icon: () => (
 | 
				
			||||||
 | 
								<NIcon>
 | 
				
			||||||
 | 
									<Delete />
 | 
				
			||||||
 | 
								</NIcon>
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					const dropdownOptionsFile: DropdownOptionsType = [
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							label: () => <NText>Download</NText>,
 | 
				
			||||||
 | 
							key: 'download',
 | 
				
			||||||
 | 
							icon: () => (
 | 
				
			||||||
 | 
								<NIcon>
 | 
				
			||||||
 | 
									<Download />
 | 
				
			||||||
 | 
								</NIcon>
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							label: () => <NText type="error">Delete</NText>,
 | 
				
			||||||
 | 
							key: 'delete',
 | 
				
			||||||
 | 
							icon: () => (
 | 
				
			||||||
 | 
								<NIcon>
 | 
				
			||||||
 | 
									<Delete />
 | 
				
			||||||
 | 
								</NIcon>
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dropdownSelect = loadingMsgWrapper(message, async (key: string) => {
 | 
				
			||||||
 | 
						dropdownShow.value = false;
 | 
				
			||||||
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
 | 
						if (!token) return;
 | 
				
			||||||
 | 
						if (!dropdownCurrentNode) return;
 | 
				
			||||||
 | 
						switch (key) {
 | 
				
			||||||
 | 
							case 'download':
 | 
				
			||||||
 | 
								if (dropdownCurrentNode.isFile)
 | 
				
			||||||
 | 
									await FS.download_file(token, dropdownCurrentNode.id);
 | 
				
			||||||
 | 
								else createZipDialog([dropdownCurrentNode.id], dialog, jwt);
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case 'delete':
 | 
				
			||||||
 | 
								dialog.warning({
 | 
				
			||||||
 | 
									title: 'Really delete?',
 | 
				
			||||||
 | 
									content: `Are you sure you want to delete "${dropdownCurrentNode.name}"`,
 | 
				
			||||||
 | 
									positiveText: 'Yes',
 | 
				
			||||||
 | 
									negativeText: 'No',
 | 
				
			||||||
 | 
									onPositiveClick: () => {
 | 
				
			||||||
 | 
										if (!dropdownCurrentNode) return;
 | 
				
			||||||
 | 
										deleteNodes.value = [dropdownCurrentNode.id];
 | 
				
			||||||
 | 
										showDeleteDialog();
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const columns: DataTableColumn<Responses.GetNodeEntry>[] = [
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							type: 'selection',
 | 
				
			||||||
 | 
							options: [
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									label: 'Select all folders',
 | 
				
			||||||
 | 
									key: 'folders',
 | 
				
			||||||
 | 
									onSelect(data) {
 | 
				
			||||||
 | 
										checkedRows.value = data
 | 
				
			||||||
 | 
											.filter((node) => !node.isFile)
 | 
				
			||||||
 | 
											.map((node) => node.id);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									label: 'Select all files',
 | 
				
			||||||
 | 
									key: 'files',
 | 
				
			||||||
 | 
									onSelect(data) {
 | 
				
			||||||
 | 
										checkedRows.value = data
 | 
				
			||||||
 | 
											.filter((node) => node.isFile)
 | 
				
			||||||
 | 
											.map((node) => node.id);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							],
 | 
				
			||||||
 | 
							disabled(node) {
 | 
				
			||||||
 | 
								return node.parent == null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							title: 'Name',
 | 
				
			||||||
 | 
							key: 'name',
 | 
				
			||||||
 | 
							minWidth: 720,
 | 
				
			||||||
 | 
							render(node) {
 | 
				
			||||||
 | 
								return (
 | 
				
			||||||
 | 
									<NLink to={`/fs/${node.id}`}>
 | 
				
			||||||
 | 
										<div>
 | 
				
			||||||
 | 
											<NIcon
 | 
				
			||||||
 | 
												size="1.2em"
 | 
				
			||||||
 | 
												color="#111"
 | 
				
			||||||
 | 
												component={
 | 
				
			||||||
 | 
													node.isFile
 | 
				
			||||||
 | 
														? DocumentBlank
 | 
				
			||||||
 | 
														: node.name == '..'
 | 
				
			||||||
 | 
														? FolderParent
 | 
				
			||||||
 | 
														: Folder
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												style="top: 0.25em; margin-right: 0.5em"
 | 
				
			||||||
 | 
											/>
 | 
				
			||||||
 | 
											{node.name}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</NLink>
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							title: 'Size',
 | 
				
			||||||
 | 
							key: 'size',
 | 
				
			||||||
 | 
							minWidth: 100,
 | 
				
			||||||
 | 
							render(node) {
 | 
				
			||||||
 | 
								return !node.isFile ? (
 | 
				
			||||||
 | 
									''
 | 
				
			||||||
 | 
								) : (
 | 
				
			||||||
 | 
									<NPopover trigger="hover">
 | 
				
			||||||
 | 
										{{
 | 
				
			||||||
 | 
											default: () => `${node.size?.toLocaleString()} bytes`,
 | 
				
			||||||
 | 
											trigger: () =>
 | 
				
			||||||
 | 
												filesize(node.size ?? 0, {
 | 
				
			||||||
 | 
													base: 2,
 | 
				
			||||||
 | 
													standard: 'jedec'
 | 
				
			||||||
 | 
												})
 | 
				
			||||||
 | 
										}}
 | 
				
			||||||
 | 
									</NPopover>
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					const previewColumns: DataTableColumn<Responses.GetNodeEntry>[] = [
 | 
				
			||||||
 | 
						columns[0],
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							title: 'Preview',
 | 
				
			||||||
 | 
							key: 'preview',
 | 
				
			||||||
 | 
							render(node) {
 | 
				
			||||||
 | 
								return node.preview ? (
 | 
				
			||||||
 | 
									<Suspense>
 | 
				
			||||||
 | 
										{{
 | 
				
			||||||
 | 
											default: () => (
 | 
				
			||||||
 | 
												<AsyncImage alt={node.name} id={node.id} />
 | 
				
			||||||
 | 
											),
 | 
				
			||||||
 | 
											fallback: () => <NSpin size="small" />
 | 
				
			||||||
 | 
										}}
 | 
				
			||||||
 | 
									</Suspense>
 | 
				
			||||||
 | 
								) : (
 | 
				
			||||||
 | 
									''
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						...columns.slice(1)
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const massDownload = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
 | 
						if (!token) return;
 | 
				
			||||||
 | 
						const nodes = checkedRows.value;
 | 
				
			||||||
 | 
						if (nodes.length == 1) {
 | 
				
			||||||
 | 
							const node = props.nodes.find((n) => n.id == nodes[0]);
 | 
				
			||||||
 | 
							if (!node) return;
 | 
				
			||||||
 | 
							if (node.isFile) await FS.download_file(token, nodes[0]);
 | 
				
			||||||
 | 
							else createZipDialog(nodes, dialog, jwt);
 | 
				
			||||||
 | 
						} else createZipDialog(nodes, dialog, jwt);
 | 
				
			||||||
 | 
						checkedRows.value = [];
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const massDelete = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
 | 
						if (!token) return;
 | 
				
			||||||
 | 
						dialog.warning({
 | 
				
			||||||
 | 
							title: 'Really delete?',
 | 
				
			||||||
 | 
							content: `Are you sure you want to delete "${checkedRows.value.length} folders/files"`,
 | 
				
			||||||
 | 
							positiveText: 'Yes',
 | 
				
			||||||
 | 
							negativeText: 'No',
 | 
				
			||||||
 | 
							onPositiveClick: loadingMsgWrapper(message, async () => {
 | 
				
			||||||
 | 
								deleteNodes.value = checkedRows.value;
 | 
				
			||||||
 | 
								showDeleteDialog();
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const selectionCell = (): SummaryCell => {
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							value:
 | 
				
			||||||
 | 
								checkedRows.value.length != 0 ? (
 | 
				
			||||||
 | 
									<NButtonGroup>
 | 
				
			||||||
 | 
										<NButton onClick={massDownload}>Download</NButton>
 | 
				
			||||||
 | 
										<NButton onClick={massDelete} type="error">
 | 
				
			||||||
 | 
											Delete
 | 
				
			||||||
 | 
										</NButton>
 | 
				
			||||||
 | 
									</NButtonGroup>
 | 
				
			||||||
 | 
								) : (
 | 
				
			||||||
 | 
									''
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							colSpan: props.showPreview ? 2 : 1
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const sizeCell = (data: Responses.GetNodeEntry[]): SummaryCell => {
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							value: (
 | 
				
			||||||
 | 
								<span>
 | 
				
			||||||
 | 
									{filesize(
 | 
				
			||||||
 | 
										data.reduce((cur, node) => cur + (node.size ?? 0), 0),
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											base: 2,
 | 
				
			||||||
 | 
											standard: 'jedec'
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
								</span>
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createPreviewSummary(data: Responses.GetNodeEntry[]) {
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							preview: selectionCell(),
 | 
				
			||||||
 | 
							size: sizeCell(data)
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createSummary(data: Responses.GetNodeEntry[]) {
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							name: selectionCell(),
 | 
				
			||||||
 | 
							size: sizeCell(data)
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function rowProps(node: Responses.GetNodeEntry) {
 | 
				
			||||||
 | 
						if (!('isFile' in node)) return {};
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							onContextmenu: (e: MouseEvent) => {
 | 
				
			||||||
 | 
								e.preventDefault();
 | 
				
			||||||
 | 
								dropdownShow.value = false;
 | 
				
			||||||
 | 
								dropdownCurrentNode = node;
 | 
				
			||||||
 | 
								dropdownOptions.value = node.isFile
 | 
				
			||||||
 | 
									? dropdownOptionsFile
 | 
				
			||||||
 | 
									: dropdownOptionsFolder;
 | 
				
			||||||
 | 
								nextTick().then(() => {
 | 
				
			||||||
 | 
									dropdownShow.value = true;
 | 
				
			||||||
 | 
									dropdownX.value = e.clientX;
 | 
				
			||||||
 | 
									dropdownY.value = e.clientY;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const rowKey = (node: Responses.GetNodeEntry): number => node.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function showDeleteDialog() {
 | 
				
			||||||
 | 
						if (deleteNodes.value.length == 0) return;
 | 
				
			||||||
 | 
						deleteDialogShow.value = true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function onShowDeleteDialog() {
 | 
				
			||||||
 | 
						await deleteDialog.value?.startDelete();
 | 
				
			||||||
 | 
						deleteDialogShow.value = false;
 | 
				
			||||||
 | 
						emit('reloadNode');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<n-image-group>
 | 
				
			||||||
 | 
							<n-data-table
 | 
				
			||||||
 | 
								:columns="showPreview ? previewColumns : columns"
 | 
				
			||||||
 | 
								:data="nodes"
 | 
				
			||||||
 | 
								:row-key="rowKey"
 | 
				
			||||||
 | 
								:row-props="rowProps"
 | 
				
			||||||
 | 
								:summary="showPreview ? createPreviewSummary : createSummary"
 | 
				
			||||||
 | 
								v-model:checked-row-keys="checkedRows"
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						</n-image-group>
 | 
				
			||||||
 | 
						<n-dropdown
 | 
				
			||||||
 | 
							placement="bottom-start"
 | 
				
			||||||
 | 
							trigger="manual"
 | 
				
			||||||
 | 
							:x="dropdownX"
 | 
				
			||||||
 | 
							:y="dropdownY"
 | 
				
			||||||
 | 
							:show="dropdownShow"
 | 
				
			||||||
 | 
							:show-arrow="true"
 | 
				
			||||||
 | 
							:options="dropdownOptions"
 | 
				
			||||||
 | 
							:on-clickoutside="() => (dropdownShow = false)"
 | 
				
			||||||
 | 
							@select="dropdownSelect"
 | 
				
			||||||
 | 
						/>
 | 
				
			||||||
 | 
						<n-modal
 | 
				
			||||||
 | 
							v-model:show="deleteDialogShow"
 | 
				
			||||||
 | 
							:close-on-esc="false"
 | 
				
			||||||
 | 
							:mask-closable="false"
 | 
				
			||||||
 | 
							:on-after-enter="onShowDeleteDialog"
 | 
				
			||||||
 | 
						>
 | 
				
			||||||
 | 
							<DeleteModal ref="deleteDialog" :nodes="deleteNodes" />
 | 
				
			||||||
 | 
						</n-modal>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped></style>
 | 
				
			||||||
@@ -1,41 +0,0 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					 | 
				
			||||||
import { defineEmits, defineProps, inject } from "vue";
 | 
					 | 
				
			||||||
import { check_token, FS, Responses } from "@/api";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  node: Responses.FS.GetNodeResponse;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const emit = defineEmits<{
 | 
					 | 
				
			||||||
  (e: "reloadNode"): void;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function del() {
 | 
					 | 
				
			||||||
  const token = await check_token(jwt);
 | 
					 | 
				
			||||||
  if (!token) return;
 | 
					 | 
				
			||||||
  await FS.delete_node(token, props.node.id);
 | 
					 | 
				
			||||||
  emit("reloadNode");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function download() {
 | 
					 | 
				
			||||||
  const token = await check_token(jwt);
 | 
					 | 
				
			||||||
  if (!token) return;
 | 
					 | 
				
			||||||
  FS.download_file(token, props.node.id);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <td>
 | 
					 | 
				
			||||||
    <router-link :to="'/fs/' + props.node.id">{{ node.name }}</router-link>
 | 
					 | 
				
			||||||
  </td>
 | 
					 | 
				
			||||||
  <td>
 | 
					 | 
				
			||||||
    <a href="#" @click="download()" v-if="props.node.isFile">Download</a>
 | 
					 | 
				
			||||||
  </td>
 | 
					 | 
				
			||||||
  <td>
 | 
					 | 
				
			||||||
    <a href="#" @click="del()" v-if="props.node.name !== '..'">delete</a>
 | 
					 | 
				
			||||||
  </td>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped></style>
 | 
					 | 
				
			||||||
@@ -1,102 +0,0 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					 | 
				
			||||||
import { defineEmits, defineProps, inject, reactive, ref, watch } from "vue";
 | 
					 | 
				
			||||||
import { FS, Responses, check_token } from "@/api";
 | 
					 | 
				
			||||||
import DirEntry from "@/components/FSView/DirEntry.vue";
 | 
					 | 
				
			||||||
import UploadFileDialog from "@/components/UploadDialog/UploadFileDialog.vue";
 | 
					 | 
				
			||||||
import { NModal } from "naive-ui";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  node: Responses.FS.GetNodeResponse;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const emit = defineEmits<{
 | 
					 | 
				
			||||||
  (e: "reloadNode"): void;
 | 
					 | 
				
			||||||
  (e: "gotoRoot"): void;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const fileInput = ref<HTMLInputElement>();
 | 
					 | 
				
			||||||
const uploadDialog = ref();
 | 
					 | 
				
			||||||
const uploadDialogShow = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const new_folder_name = ref("");
 | 
					 | 
				
			||||||
const files = ref<File[]>([]);
 | 
					 | 
				
			||||||
const nodes = ref<Responses.FS.GetNodeResponse[]>([]);
 | 
					 | 
				
			||||||
const hasParent = ref(false);
 | 
					 | 
				
			||||||
const parentNode = reactive<Responses.FS.GetNodeResponse>({
 | 
					 | 
				
			||||||
  id: 0,
 | 
					 | 
				
			||||||
  statusCode: 200,
 | 
					 | 
				
			||||||
  isFile: false,
 | 
					 | 
				
			||||||
  parent: null,
 | 
					 | 
				
			||||||
  name: "..",
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
  () => props.node,
 | 
					 | 
				
			||||||
  async (to) => {
 | 
					 | 
				
			||||||
    parentNode.id = to.parent ?? 0;
 | 
					 | 
				
			||||||
    hasParent.value = to.parent != null;
 | 
					 | 
				
			||||||
    nodes.value = [];
 | 
					 | 
				
			||||||
    const token = await check_token(jwt);
 | 
					 | 
				
			||||||
    if (!token) return;
 | 
					 | 
				
			||||||
    await Promise.all(
 | 
					 | 
				
			||||||
      to.children?.map(async (child) => {
 | 
					 | 
				
			||||||
        nodes.value.push(
 | 
					 | 
				
			||||||
          (await FS.get_node(token, child)) as Responses.FS.GetNodeResponse
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }) ?? []
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  { immediate: true }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function newFolder() {
 | 
					 | 
				
			||||||
  const token = await check_token(jwt);
 | 
					 | 
				
			||||||
  if (!token) return;
 | 
					 | 
				
			||||||
  await FS.create_folder(token, props.node.id, new_folder_name.value);
 | 
					 | 
				
			||||||
  emit("reloadNode");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function uploadFiles() {
 | 
					 | 
				
			||||||
  files.value = Array.from(fileInput.value?.files ?? []);
 | 
					 | 
				
			||||||
  if (files.value.length == 0) return;
 | 
					 | 
				
			||||||
  uploadDialogShow.value = true;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
async function uploadFilesDialogOpen() {
 | 
					 | 
				
			||||||
  await uploadDialog.value?.startUpload(props.node.id);
 | 
					 | 
				
			||||||
  uploadDialogShow.value = false;
 | 
					 | 
				
			||||||
  if (fileInput.value) fileInput.value.value = "";
 | 
					 | 
				
			||||||
  emit("reloadNode");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <input type="text" placeholder="Folder name" v-model="new_folder_name" />
 | 
					 | 
				
			||||||
    <a href="#" @click="newFolder()">create folder</a>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <input type="file" ref="fileInput" multiple />
 | 
					 | 
				
			||||||
    <a href="#" @click="uploadFiles()">upload files</a>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
  <table>
 | 
					 | 
				
			||||||
    <tr v-if="hasParent">
 | 
					 | 
				
			||||||
      <DirEntry :node="parentNode" @reloadNode="emit('reloadNode')" />
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
    <tr v-for="n in nodes" :key="n.id">
 | 
					 | 
				
			||||||
      <DirEntry :node="n" @reloadNode="emit('reloadNode')" />
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
  </table>
 | 
					 | 
				
			||||||
  <n-modal
 | 
					 | 
				
			||||||
    v-model:show="uploadDialogShow"
 | 
					 | 
				
			||||||
    :close-on-esc="false"
 | 
					 | 
				
			||||||
    :mask-closable="false"
 | 
					 | 
				
			||||||
    :on-after-enter="uploadFilesDialogOpen"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <UploadFileDialog ref="uploadDialog" :files="files" />
 | 
					 | 
				
			||||||
  </n-modal>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped></style>
 | 
					 | 
				
			||||||
@@ -1,39 +0,0 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					 | 
				
			||||||
import { defineProps, inject } from "vue";
 | 
					 | 
				
			||||||
import { check_token, FS, Responses } from "@/api";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  node: Responses.FS.GetNodeResponse;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function del() {
 | 
					 | 
				
			||||||
  const token = await check_token(jwt);
 | 
					 | 
				
			||||||
  if (!token) return;
 | 
					 | 
				
			||||||
  await FS.delete_node(token, props.node.id);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function download() {
 | 
					 | 
				
			||||||
  const token = await check_token(jwt);
 | 
					 | 
				
			||||||
  if (!token) return;
 | 
					 | 
				
			||||||
  FS.download_file(token, props.node.id);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <router-link :to="'/fs/' + props.node.parent ?? 0">..</router-link>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <a href="#" @click="download()" v-if="props.node.isFile">Download</a>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <router-link :to="'/fs/' + props.node.parent ?? 0" @click="del()">
 | 
					 | 
				
			||||||
      delete
 | 
					 | 
				
			||||||
    </router-link>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped></style>
 | 
					 | 
				
			||||||
							
								
								
									
										47
									
								
								frontend/src/components/FileViewer/BlobDownload.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								frontend/src/components/FileViewer/BlobDownload.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					import { ref } from 'vue';
 | 
				
			||||||
 | 
					import { NProgress } from 'naive-ui';
 | 
				
			||||||
 | 
					import filesize from 'filesize';
 | 
				
			||||||
 | 
					import { Music, Video, Image } from '@vicons/carbon';
 | 
				
			||||||
 | 
					import type { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function createBlobDialog(
 | 
				
			||||||
 | 
						dialog: DialogApiInjection,
 | 
				
			||||||
 | 
						audio: boolean,
 | 
				
			||||||
 | 
						video: boolean
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						const progress = ref(0);
 | 
				
			||||||
 | 
						const total = ref(1);
 | 
				
			||||||
 | 
						const percentage = ref(0);
 | 
				
			||||||
 | 
						const dia = dialog.create({
 | 
				
			||||||
 | 
							title:
 | 
				
			||||||
 | 
								'Loading ' + (video ? 'video' : audio ? 'audio' : 'image') + '...',
 | 
				
			||||||
 | 
							closable: false,
 | 
				
			||||||
 | 
							closeOnEsc: false,
 | 
				
			||||||
 | 
							maskClosable: false,
 | 
				
			||||||
 | 
							icon: () => (video ? <Video /> : audio ? <Music /> : <Image />),
 | 
				
			||||||
 | 
							content: () => (
 | 
				
			||||||
 | 
								<NProgress
 | 
				
			||||||
 | 
									type="line"
 | 
				
			||||||
 | 
									percentage={percentage.value}
 | 
				
			||||||
 | 
									height={20}
 | 
				
			||||||
 | 
									status="info"
 | 
				
			||||||
 | 
									showIndicator={false}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
							action: () => (
 | 
				
			||||||
 | 
								<div>
 | 
				
			||||||
 | 
									{filesize(progress.value, {
 | 
				
			||||||
 | 
										base: 2,
 | 
				
			||||||
 | 
										standard: 'jedec'
 | 
				
			||||||
 | 
									})}
 | 
				
			||||||
 | 
									/
 | 
				
			||||||
 | 
									{filesize(total.value, {
 | 
				
			||||||
 | 
										base: 2,
 | 
				
			||||||
 | 
										standard: 'jedec'
 | 
				
			||||||
 | 
									})}
 | 
				
			||||||
 | 
									- {Math.floor(percentage.value * 1000) / 1000}%
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						return { progress, total, percentage, dia };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										126
									
								
								frontend/src/components/FileViewer/FileViewer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								frontend/src/components/FileViewer/FileViewer.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,126 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { TokenInjectType, Responses } from '@/api';
 | 
				
			||||||
 | 
					import { inject, ref, watch } from 'vue';
 | 
				
			||||||
 | 
					import { Download, Play } from '@vicons/carbon';
 | 
				
			||||||
 | 
					import { useDialog, NGrid, NGi, NButton, NImage, NSpin, NIcon } from 'naive-ui';
 | 
				
			||||||
 | 
					import { check_token, FS, isErrorResponse } from '@/api';
 | 
				
			||||||
 | 
					import createBlobDialog from '@/components/FileViewer/BlobDownload';
 | 
				
			||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
						node: Responses.GetNode;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dialog = useDialog();
 | 
				
			||||||
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum fileTypes {
 | 
				
			||||||
 | 
						UNKNOWN,
 | 
				
			||||||
 | 
						LOADING,
 | 
				
			||||||
 | 
						IMAGE,
 | 
				
			||||||
 | 
						AUDIO,
 | 
				
			||||||
 | 
						VIDEO
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const fileType = ref<fileTypes>(fileTypes.UNKNOWN);
 | 
				
			||||||
 | 
					const src = ref('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function download() {
 | 
				
			||||||
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
 | 
						if (!token) return;
 | 
				
			||||||
 | 
						FS.download_file(token, props.node.id);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function loadContent() {
 | 
				
			||||||
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
 | 
						if (!token) return;
 | 
				
			||||||
 | 
						const { progress, total, percentage, dia } = createBlobDialog(
 | 
				
			||||||
 | 
							dialog,
 | 
				
			||||||
 | 
							fileType.value === fileTypes.AUDIO,
 | 
				
			||||||
 | 
							fileType.value === fileTypes.VIDEO
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						total.value = props.node.size ?? 1;
 | 
				
			||||||
 | 
						const params = new URLSearchParams();
 | 
				
			||||||
 | 
						params.append('jwtToken', token);
 | 
				
			||||||
 | 
						params.append('id', props.node.id.toString());
 | 
				
			||||||
 | 
						const resp = await axios.post('/api/fs/download', params, {
 | 
				
			||||||
 | 
							responseType: 'blob',
 | 
				
			||||||
 | 
							onDownloadProgress: (e: ProgressEvent) => {
 | 
				
			||||||
 | 
								progress.value = e.loaded;
 | 
				
			||||||
 | 
								percentage.value = (e.loaded / e.total) * 100;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						dia.destroy();
 | 
				
			||||||
 | 
						if (resp.status != 200) return;
 | 
				
			||||||
 | 
						src.value = URL.createObjectURL(resp.data as Blob);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getType(node: Responses.GetNode) {
 | 
				
			||||||
 | 
						fileType.value = fileTypes.LOADING;
 | 
				
			||||||
 | 
						if (src.value.startsWith('blob')) URL.revokeObjectURL(src.value);
 | 
				
			||||||
 | 
						src.value = '';
 | 
				
			||||||
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
 | 
						if (!token) return;
 | 
				
			||||||
 | 
						const resp = await FS.get_type(token, node.id);
 | 
				
			||||||
 | 
						if (isErrorResponse(resp)) return;
 | 
				
			||||||
 | 
						if (resp.type.startsWith('image')) {
 | 
				
			||||||
 | 
							fileType.value = fileTypes.IMAGE;
 | 
				
			||||||
 | 
							await loadContent();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (resp.type.startsWith('audio')) fileType.value = fileTypes.AUDIO;
 | 
				
			||||||
 | 
						if (resp.type.startsWith('video')) fileType.value = fileTypes.VIDEO;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
						() => props.node,
 | 
				
			||||||
 | 
						async (to) => {
 | 
				
			||||||
 | 
							await getType(to);
 | 
				
			||||||
 | 
							if (fileType.value === fileTypes.LOADING)
 | 
				
			||||||
 | 
								fileType.value = fileTypes.UNKNOWN;
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{ immediate: true }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<n-grid cols="1" x-gap="16" y-gap="16">
 | 
				
			||||||
 | 
							<n-gi style="text-align: right">
 | 
				
			||||||
 | 
								<n-button @click="download()">
 | 
				
			||||||
 | 
									<template #icon>
 | 
				
			||||||
 | 
										<n-icon><Download /></n-icon>
 | 
				
			||||||
 | 
									</template>
 | 
				
			||||||
 | 
									Download
 | 
				
			||||||
 | 
								</n-button>
 | 
				
			||||||
 | 
							</n-gi>
 | 
				
			||||||
 | 
							<n-gi style="text-align: center">
 | 
				
			||||||
 | 
								<n-spin v-if="fileType === fileTypes.LOADING" size="large" />
 | 
				
			||||||
 | 
								<template v-else-if="fileType !== fileTypes.UNKNOWN">
 | 
				
			||||||
 | 
									<video
 | 
				
			||||||
 | 
										v-if="fileType === fileTypes.VIDEO && src !== ''"
 | 
				
			||||||
 | 
										:src="src"
 | 
				
			||||||
 | 
										controls
 | 
				
			||||||
 | 
										autoplay
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
									<audio
 | 
				
			||||||
 | 
										v-else-if="fileType === fileTypes.AUDIO && src !== ''"
 | 
				
			||||||
 | 
										:src="src"
 | 
				
			||||||
 | 
										controls
 | 
				
			||||||
 | 
										autoplay
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
									<n-image
 | 
				
			||||||
 | 
										v-else-if="fileType === fileTypes.IMAGE && src !== ''"
 | 
				
			||||||
 | 
										:src="src"
 | 
				
			||||||
 | 
										:alt="node.name"
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
									<n-button v-else-if="fileType !== fileTypes.IMAGE" @click="loadContent">
 | 
				
			||||||
 | 
										<template #icon>
 | 
				
			||||||
 | 
											<n-icon><Play /></n-icon>
 | 
				
			||||||
 | 
										</template>
 | 
				
			||||||
 | 
										Load and play
 | 
				
			||||||
 | 
									</n-button>
 | 
				
			||||||
 | 
								</template>
 | 
				
			||||||
 | 
							</n-gi>
 | 
				
			||||||
 | 
						</n-grid>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped></style>
 | 
				
			||||||
@@ -1,140 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
  <div class="hello">
 | 
					 | 
				
			||||||
    <h1>{{ msg }}</h1>
 | 
					 | 
				
			||||||
    <p>
 | 
					 | 
				
			||||||
      For a guide and recipes on how to configure / customize this project,<br />
 | 
					 | 
				
			||||||
      check out the
 | 
					 | 
				
			||||||
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener"
 | 
					 | 
				
			||||||
        >vue-cli documentation</a
 | 
					 | 
				
			||||||
      >.
 | 
					 | 
				
			||||||
    </p>
 | 
					 | 
				
			||||||
    <h3>Installed CLI Plugins</h3>
 | 
					 | 
				
			||||||
    <ul>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener"
 | 
					 | 
				
			||||||
          >babel</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener"
 | 
					 | 
				
			||||||
          >router</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener"
 | 
					 | 
				
			||||||
          >vuex</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener"
 | 
					 | 
				
			||||||
          >eslint</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener"
 | 
					 | 
				
			||||||
          >typescript</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
    </ul>
 | 
					 | 
				
			||||||
    <h3>Essential Links</h3>
 | 
					 | 
				
			||||||
    <ul>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a href="https://forum.vuejs.org" target="_blank" rel="noopener"
 | 
					 | 
				
			||||||
          >Forum</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a href="https://chat.vuejs.org" target="_blank" rel="noopener"
 | 
					 | 
				
			||||||
          >Community Chat</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
 | 
					 | 
				
			||||||
          >Twitter</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
    </ul>
 | 
					 | 
				
			||||||
    <h3>Ecosystem</h3>
 | 
					 | 
				
			||||||
    <ul>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a href="https://router.vuejs.org" target="_blank" rel="noopener"
 | 
					 | 
				
			||||||
          >vue-router</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://github.com/vuejs/vue-devtools#vue-devtools"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener"
 | 
					 | 
				
			||||||
          >vue-devtools</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
 | 
					 | 
				
			||||||
          >vue-loader</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
      <li>
 | 
					 | 
				
			||||||
        <a
 | 
					 | 
				
			||||||
          href="https://github.com/vuejs/awesome-vue"
 | 
					 | 
				
			||||||
          target="_blank"
 | 
					 | 
				
			||||||
          rel="noopener"
 | 
					 | 
				
			||||||
          >awesome-vue</a
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </li>
 | 
					 | 
				
			||||||
    </ul>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { defineComponent } from "vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: "HelloWorld",
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    msg: String,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
 | 
					 | 
				
			||||||
<style scoped lang="scss">
 | 
					 | 
				
			||||||
h3 {
 | 
					 | 
				
			||||||
  margin: 40px 0 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
ul {
 | 
					 | 
				
			||||||
  list-style-type: none;
 | 
					 | 
				
			||||||
  padding: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
li {
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
  margin: 0 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
a {
 | 
					 | 
				
			||||||
  color: #42b983;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
							
								
								
									
										13
									
								
								frontend/src/components/NLink.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/components/NLink.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { NA } from 'naive-ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineProps<{
 | 
				
			||||||
 | 
						to: string;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<router-link :to="to" #="{ navigate, href }" custom>
 | 
				
			||||||
 | 
							<n-a :href="href" @click="navigate"><slot /></n-a>
 | 
				
			||||||
 | 
						</router-link>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
@@ -1,52 +1,93 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { Status } from "naive-ui/es/progress/src/interface";
 | 
					import type { Status } from 'naive-ui/es/progress/src/interface';
 | 
				
			||||||
import { defineProps, defineExpose, ref } from "vue";
 | 
					import type { UploadFile } from '@/api';
 | 
				
			||||||
import { isErrorResponse, FS } from "@/api";
 | 
					import { ref } from 'vue';
 | 
				
			||||||
import { NProgress } from "naive-ui";
 | 
					import { isErrorResponse, FS } from '@/api';
 | 
				
			||||||
import filesize from "filesize";
 | 
					import { NProgress } from 'naive-ui';
 | 
				
			||||||
 | 
					import filesize from 'filesize';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
  file: File;
 | 
						file: UploadFile;
 | 
				
			||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const progress = ref(0);
 | 
					const progress = ref(0);
 | 
				
			||||||
const percentage = ref(0);
 | 
					const percentage = ref(0);
 | 
				
			||||||
const err = ref("");
 | 
					const err = ref('');
 | 
				
			||||||
const status = ref<Status>("info");
 | 
					const status = ref<Status>('info');
 | 
				
			||||||
 | 
					const shown = ref(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function startUpload(parent: number, token: string) {
 | 
					async function startUpload(token: string, done: () => void) {
 | 
				
			||||||
  const resp = await FS.upload_file(token, parent, props.file, (e) => {
 | 
						const resp = await FS.upload_file(token, props.file, (e) => {
 | 
				
			||||||
    progress.value = e.loaded;
 | 
							progress.value = e.loaded;
 | 
				
			||||||
    percentage.value = (e.loaded / e.total) * 100;
 | 
							percentage.value = (e.loaded / e.total) * 100;
 | 
				
			||||||
  });
 | 
							if (e.loaded == e.total) done();
 | 
				
			||||||
  percentage.value = 100;
 | 
						});
 | 
				
			||||||
  if (isErrorResponse(resp)) {
 | 
						percentage.value = 100;
 | 
				
			||||||
    err.value = resp.message ?? "Error";
 | 
						if (isErrorResponse(resp)) {
 | 
				
			||||||
    status.value = "error";
 | 
							err.value = resp.message ?? 'Error';
 | 
				
			||||||
  } else status.value = "success";
 | 
							status.value = 'error';
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							status.value = 'success';
 | 
				
			||||||
 | 
							shown.value = false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineExpose({
 | 
					defineExpose({
 | 
				
			||||||
  startUpload,
 | 
						startUpload
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div v-if="percentage < 100">
 | 
						<Transition name="slide-up">
 | 
				
			||||||
    {{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} -
 | 
							<div class="container" v-show="shown">
 | 
				
			||||||
    {{ Math.floor(percentage * 1000) / 1000 }}%
 | 
								<div v-if="percentage < 100">
 | 
				
			||||||
  </div>
 | 
									{{ file.fullName }} -
 | 
				
			||||||
  <div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div>
 | 
									{{
 | 
				
			||||||
  <div v-else>{{ file.name }} - Completed</div>
 | 
										filesize(progress, {
 | 
				
			||||||
  <n-progress
 | 
											base: 2,
 | 
				
			||||||
    type="line"
 | 
											standard: 'jedec'
 | 
				
			||||||
    :percentage="percentage"
 | 
										})
 | 
				
			||||||
    :height="20"
 | 
									}}
 | 
				
			||||||
    :status="status"
 | 
									/
 | 
				
			||||||
    border-radius="10px 0"
 | 
									{{
 | 
				
			||||||
    fill-border-radius="10px 0"
 | 
										filesize(file.file.size, {
 | 
				
			||||||
    :show-indicator="false"
 | 
											base: 2,
 | 
				
			||||||
  />
 | 
											standard: 'jedec'
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
									}}
 | 
				
			||||||
 | 
									- {{ Math.floor(percentage * 1000) / 1000 }}%
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div v-else-if="err !== ''">
 | 
				
			||||||
 | 
									{{ file.fullName }} - Error: {{ err }}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div v-else>{{ file.fullName }} - Completed</div>
 | 
				
			||||||
 | 
								<n-progress
 | 
				
			||||||
 | 
									type="line"
 | 
				
			||||||
 | 
									:percentage="percentage"
 | 
				
			||||||
 | 
									:height="20"
 | 
				
			||||||
 | 
									:status="status"
 | 
				
			||||||
 | 
									border-radius="10px 0"
 | 
				
			||||||
 | 
									fill-border-radius="10px 0"
 | 
				
			||||||
 | 
									:show-indicator="false"
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</Transition>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped></style>
 | 
					<style scoped lang="scss">
 | 
				
			||||||
 | 
					.container {
 | 
				
			||||||
 | 
						height: 60px;
 | 
				
			||||||
 | 
						padding: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.slide-up-leave-active {
 | 
				
			||||||
 | 
						transition: all 2s ease-out;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.slide-up-leave-to {
 | 
				
			||||||
 | 
						height: 0;
 | 
				
			||||||
 | 
						padding: 0 8px;
 | 
				
			||||||
 | 
						opacity: 0;
 | 
				
			||||||
 | 
						transform: translateY(-60px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										203
									
								
								frontend/src/components/UploadDialog/UploadField.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								frontend/src/components/UploadDialog/UploadField.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,203 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { TokenInjectType, Responses, UploadFile } from '@/api';
 | 
				
			||||||
 | 
					import { inject, ref } from 'vue';
 | 
				
			||||||
 | 
					import { useMessage, NModal, NText, NIcon } from 'naive-ui';
 | 
				
			||||||
 | 
					import { CloudUpload } from '@vicons/carbon';
 | 
				
			||||||
 | 
					import { FS, check_token, isErrorResponse } from '@/api';
 | 
				
			||||||
 | 
					import UploadFileDialog from '@/components/UploadDialog/UploadFileDialog.vue';
 | 
				
			||||||
 | 
					import { loadingMsgWrapper } from '@/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const message = useMessage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
						node: Responses.GetNode;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<{
 | 
				
			||||||
 | 
						(e: 'reloadNode'): void;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uploadArea = ref<HTMLDivElement>();
 | 
				
			||||||
 | 
					const fileInput = ref<HTMLInputElement>();
 | 
				
			||||||
 | 
					const uploadDialog = ref();
 | 
				
			||||||
 | 
					const uploadDialogShow = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const files = ref<UploadFile[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function startDrag() {
 | 
				
			||||||
 | 
						uploadArea.value?.classList.add('uploadActive');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function stopDrag() {
 | 
				
			||||||
 | 
						uploadArea.value?.classList.remove('uploadActive');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openBrowser() {
 | 
				
			||||||
 | 
						fileInput.value?.click();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function browserChanged(event: Event) {
 | 
				
			||||||
 | 
						files.value = Array.from(
 | 
				
			||||||
 | 
							(event.target as HTMLInputElement).files ?? []
 | 
				
			||||||
 | 
						).map((file) => {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								parent: props.node.id,
 | 
				
			||||||
 | 
								fullName: file.name,
 | 
				
			||||||
 | 
								file
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						uploadFiles();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FileSystemDirectoryReader {
 | 
				
			||||||
 | 
						readEntries(
 | 
				
			||||||
 | 
							successCallback: (entries: FileSystemEntry[]) => void,
 | 
				
			||||||
 | 
							errorCallback?: (err: DOMException) => void
 | 
				
			||||||
 | 
						): void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FileSystemEntry {
 | 
				
			||||||
 | 
						readonly fullPath: string;
 | 
				
			||||||
 | 
						readonly isDirectory: boolean;
 | 
				
			||||||
 | 
						readonly isFile: boolean;
 | 
				
			||||||
 | 
						readonly name: string;
 | 
				
			||||||
 | 
						file(
 | 
				
			||||||
 | 
							successCallback: (file: File) => void,
 | 
				
			||||||
 | 
							errorCallback?: (err: DOMException) => void
 | 
				
			||||||
 | 
						): void;
 | 
				
			||||||
 | 
						createReader(): FileSystemDirectoryReader;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const asyncReadEntries = async (
 | 
				
			||||||
 | 
						reader: FileSystemDirectoryReader
 | 
				
			||||||
 | 
					): Promise<FileSystemEntry[]> =>
 | 
				
			||||||
 | 
						new Promise((resolve, reject) => reader.readEntries(resolve, reject));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getFile = async (entry: FileSystemEntry): Promise<File> =>
 | 
				
			||||||
 | 
						new Promise((resolve, reject) => entry.file(resolve, reject));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function processDirOrFile(
 | 
				
			||||||
 | 
						entry: FileSystemEntry,
 | 
				
			||||||
 | 
						parent: number,
 | 
				
			||||||
 | 
						token: string
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						if (entry.isDirectory) {
 | 
				
			||||||
 | 
							const resp = await FS.create_folder(token, parent, entry.name);
 | 
				
			||||||
 | 
							if (isErrorResponse(resp)) return;
 | 
				
			||||||
 | 
							if ('exists' in resp && resp.isFile) return;
 | 
				
			||||||
 | 
							const reader = entry.createReader();
 | 
				
			||||||
 | 
							let entries = [];
 | 
				
			||||||
 | 
							do {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									entries = await asyncReadEntries(reader);
 | 
				
			||||||
 | 
									entries.forEach((e) => processDirOrFile(e, resp.id, token));
 | 
				
			||||||
 | 
								} catch {
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} while (entries.length != 0);
 | 
				
			||||||
 | 
						} else
 | 
				
			||||||
 | 
							files.value.push({
 | 
				
			||||||
 | 
								parent: parent,
 | 
				
			||||||
 | 
								fullName: entry.fullPath.slice(1),
 | 
				
			||||||
 | 
								file: await getFile(entry)
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const filesDropped = loadingMsgWrapper(message, async (event: DragEvent) => {
 | 
				
			||||||
 | 
						stopDrag();
 | 
				
			||||||
 | 
						if (!event.dataTransfer) return;
 | 
				
			||||||
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
 | 
						if (!token) return;
 | 
				
			||||||
 | 
						files.value = [];
 | 
				
			||||||
 | 
						for (const file of event.dataTransfer.items) {
 | 
				
			||||||
 | 
							const entry = file.webkitGetAsEntry();
 | 
				
			||||||
 | 
							if (entry)
 | 
				
			||||||
 | 
								await processDirOrFile(
 | 
				
			||||||
 | 
									entry as unknown as FileSystemEntry,
 | 
				
			||||||
 | 
									props.node.id,
 | 
				
			||||||
 | 
									token
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						uploadFiles();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function uploadFiles() {
 | 
				
			||||||
 | 
						if (files.value.length == 0) return;
 | 
				
			||||||
 | 
						uploadDialogShow.value = true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function uploadFilesDialogOpen() {
 | 
				
			||||||
 | 
						await uploadDialog.value?.startUpload();
 | 
				
			||||||
 | 
						uploadDialogShow.value = false;
 | 
				
			||||||
 | 
						if (fileInput.value) fileInput.value.value = '';
 | 
				
			||||||
 | 
						emit('reloadNode');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<div
 | 
				
			||||||
 | 
							class="uploadArea"
 | 
				
			||||||
 | 
							ref="uploadArea"
 | 
				
			||||||
 | 
							@drop.prevent
 | 
				
			||||||
 | 
							@dragenter.prevent
 | 
				
			||||||
 | 
							@dragover.prevent
 | 
				
			||||||
 | 
							@dragleave.prevent
 | 
				
			||||||
 | 
							@dragend.prevent
 | 
				
			||||||
 | 
							@click="openBrowser"
 | 
				
			||||||
 | 
							@drop="filesDropped"
 | 
				
			||||||
 | 
							@dragenter="startDrag"
 | 
				
			||||||
 | 
							@dragover="startDrag"
 | 
				
			||||||
 | 
							@dragleave="stopDrag"
 | 
				
			||||||
 | 
							@dragend="stopDrag"
 | 
				
			||||||
 | 
						>
 | 
				
			||||||
 | 
							<input type="file" ref="fileInput" multiple @input="browserChanged" />
 | 
				
			||||||
 | 
							<div>
 | 
				
			||||||
 | 
								<n-icon size="2em">
 | 
				
			||||||
 | 
									<CloudUpload />
 | 
				
			||||||
 | 
								</n-icon>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<n-text>
 | 
				
			||||||
 | 
								Click or drag here to upload files
 | 
				
			||||||
 | 
							</n-text>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<n-modal
 | 
				
			||||||
 | 
							v-model:show="uploadDialogShow"
 | 
				
			||||||
 | 
							:close-on-esc="false"
 | 
				
			||||||
 | 
							:mask-closable="false"
 | 
				
			||||||
 | 
							:on-after-enter="uploadFilesDialogOpen"
 | 
				
			||||||
 | 
						>
 | 
				
			||||||
 | 
							<UploadFileDialog ref="uploadDialog" :files="files" />
 | 
				
			||||||
 | 
						</n-modal>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped lang="scss">
 | 
				
			||||||
 | 
					.uploadArea {
 | 
				
			||||||
 | 
						border: 1px dashed #ddd;
 | 
				
			||||||
 | 
						border-radius: 3px;
 | 
				
			||||||
 | 
						cursor: pointer;
 | 
				
			||||||
 | 
						background-color: rgb(250, 250, 252);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						text-align: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						transition: border-color 250ms ease-out, background-color 250ms ease-out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						padding: 20px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						input {
 | 
				
			||||||
 | 
							display: block;
 | 
				
			||||||
 | 
							width: 0;
 | 
				
			||||||
 | 
							height: 0;
 | 
				
			||||||
 | 
							opacity: 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.uploadArea:hover {
 | 
				
			||||||
 | 
						border-color: #888;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.uploadActive {
 | 
				
			||||||
 | 
						background-color: rgb(240, 252, 240);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -1,44 +1,44 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					import type { TokenInjectType, UploadFile } from '@/api';
 | 
				
			||||||
import { defineProps, defineExpose, ref, inject } from "vue";
 | 
					import { ref, inject } from 'vue';
 | 
				
			||||||
import { check_token } from "@/api";
 | 
					import { check_token } from '@/api';
 | 
				
			||||||
import UploadEntry from "@/components/UploadDialog/UploadEntry.vue";
 | 
					import UploadEntry from '@/components/UploadDialog/UploadEntry.vue';
 | 
				
			||||||
import { NCard } from "naive-ui";
 | 
					import { NCard } from 'naive-ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const entries = ref<typeof UploadEntry[]>([]);
 | 
					const entries = ref<typeof UploadEntry[]>([]);
 | 
				
			||||||
const done = ref(false);
 | 
					 | 
				
			||||||
let canCloseResolve: (value: unknown) => void = () => null;
 | 
					 | 
				
			||||||
const canClose = new Promise((r) => (canCloseResolve = r));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function startUpload(parent: number) {
 | 
					async function startUpload() {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  if (!token) return;
 | 
						if (!token) return;
 | 
				
			||||||
  await Promise.all(
 | 
						const ents: typeof UploadEntry[] = entries.value;
 | 
				
			||||||
    entries.value.map((entry) => entry.startUpload(parent, token))
 | 
						const allProms: Promise<void>[] = [];
 | 
				
			||||||
  );
 | 
						for (const entry of ents) {
 | 
				
			||||||
  done.value = true;
 | 
							await new Promise<void>((resolve) =>
 | 
				
			||||||
  await canClose;
 | 
								allProms.push(entry.startUpload(token, resolve))
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						await Promise.all(allProms);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineExpose({
 | 
					defineExpose({
 | 
				
			||||||
  startUpload,
 | 
						startUpload
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
defineProps<{
 | 
					defineProps<{
 | 
				
			||||||
  files: File[];
 | 
						files: UploadFile[];
 | 
				
			||||||
}>();
 | 
					}>();
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <n-card title="Upload Files">
 | 
						<n-card title="Uploading files" style="margin: 20px">
 | 
				
			||||||
    <div>
 | 
							<UploadEntry
 | 
				
			||||||
      <UploadEntry v-for="f in files" :key="f.name" ref="entries" :file="f" />
 | 
								v-for="f in files"
 | 
				
			||||||
    </div>
 | 
								:key="f.file.name"
 | 
				
			||||||
    <div>
 | 
								ref="entries"
 | 
				
			||||||
      <button v-if="done" @click="canCloseResolve(null)">Close</button>
 | 
								:file="f"
 | 
				
			||||||
    </div>
 | 
							/>
 | 
				
			||||||
  </n-card>
 | 
						</n-card>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped></style>
 | 
					<style scoped></style>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										73
									
								
								frontend/src/components/UserChangePw.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								frontend/src/components/UserChangePw.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { TokenInjectType } from '@/api';
 | 
				
			||||||
 | 
					import { inject, ref } from 'vue';
 | 
				
			||||||
 | 
					import { Auth, check_token, isErrorResponse } from '@/api';
 | 
				
			||||||
 | 
					import { useMessage, NInput, NGrid, NGi, NButton, NCard } from 'naive-ui';
 | 
				
			||||||
 | 
					import { loadingMsgWrapper } from '@/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const message = useMessage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const oldPw = ref('');
 | 
				
			||||||
 | 
					const newPw = ref('');
 | 
				
			||||||
 | 
					const newPw2 = ref('');
 | 
				
			||||||
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const changePw = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
 | 
						if (oldPw.value === '' || newPw.value === '' || newPw2.value === '') {
 | 
				
			||||||
 | 
							message.error('Password missing');
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (newPw.value !== newPw2.value) {
 | 
				
			||||||
 | 
							message.error("Passwords don't match");
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
 | 
						if (!token) return;
 | 
				
			||||||
 | 
						const res = await Auth.change_password(oldPw.value, newPw.value, token);
 | 
				
			||||||
 | 
						if (isErrorResponse(res))
 | 
				
			||||||
 | 
							message.error(`Password change failed: ${res.message}`);
 | 
				
			||||||
 | 
						else jwt.logout();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onKey(event: KeyboardEvent) {
 | 
				
			||||||
 | 
						if (event.key == 'Enter') changePw();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<n-card title="Change password" embedded>
 | 
				
			||||||
 | 
							<n-grid cols="1" x-gap="16" y-gap="16">
 | 
				
			||||||
 | 
								<n-gi>
 | 
				
			||||||
 | 
									<n-input
 | 
				
			||||||
 | 
										type="password"
 | 
				
			||||||
 | 
										placeholder="Old password"
 | 
				
			||||||
 | 
										v-model:value="oldPw"
 | 
				
			||||||
 | 
										@keyup="onKey"
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								</n-gi>
 | 
				
			||||||
 | 
								<n-gi>
 | 
				
			||||||
 | 
									<n-input
 | 
				
			||||||
 | 
										type="password"
 | 
				
			||||||
 | 
										placeholder="New password"
 | 
				
			||||||
 | 
										v-model:value="newPw"
 | 
				
			||||||
 | 
										@keyup="onKey"
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								</n-gi>
 | 
				
			||||||
 | 
								<n-gi>
 | 
				
			||||||
 | 
									<n-input
 | 
				
			||||||
 | 
										type="password"
 | 
				
			||||||
 | 
										placeholder="Repeat new password"
 | 
				
			||||||
 | 
										v-model:value="newPw2"
 | 
				
			||||||
 | 
										@keyup="onKey"
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								</n-gi>
 | 
				
			||||||
 | 
								<n-gi>
 | 
				
			||||||
 | 
									<n-button type="info" @click="changePw">
 | 
				
			||||||
 | 
										Change password
 | 
				
			||||||
 | 
									</n-button>
 | 
				
			||||||
 | 
								</n-gi>
 | 
				
			||||||
 | 
							</n-grid>
 | 
				
			||||||
 | 
						</n-card>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped></style>
 | 
				
			||||||
@@ -1,8 +1,150 @@
 | 
				
			|||||||
export * as Requests from "./requests";
 | 
					export enum UserRole {
 | 
				
			||||||
export * as Responses from "./responses";
 | 
						ADMIN = 2,
 | 
				
			||||||
export {
 | 
						USER = 1,
 | 
				
			||||||
  UserRole,
 | 
						DISABLED = 0
 | 
				
			||||||
  validateSync,
 | 
					}
 | 
				
			||||||
  validateAsync,
 | 
					
 | 
				
			||||||
  validateAsyncInline,
 | 
					export interface UploadFile {
 | 
				
			||||||
} from "./utils";
 | 
						parent: number;
 | 
				
			||||||
 | 
						fullName: string;
 | 
				
			||||||
 | 
						file: File;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/no-namespace
 | 
				
			||||||
 | 
					export namespace Requests {
 | 
				
			||||||
 | 
						// eslint-disable-next-line @typescript-eslint/no-empty-interface
 | 
				
			||||||
 | 
						export interface Base {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface Admin extends Base {
 | 
				
			||||||
 | 
							user: number;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface SetUserRole extends Admin {
 | 
				
			||||||
 | 
							role: UserRole;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface SignUp extends Base {
 | 
				
			||||||
 | 
							username: string;
 | 
				
			||||||
 | 
							password: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface Login extends SignUp {
 | 
				
			||||||
 | 
							otp?: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface TfaSetup extends Base {
 | 
				
			||||||
 | 
							mail: boolean;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface TfaComplete extends Base {
 | 
				
			||||||
 | 
							mail: boolean;
 | 
				
			||||||
 | 
							code: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface ChangePassword extends Base {
 | 
				
			||||||
 | 
							oldPassword: string;
 | 
				
			||||||
 | 
							newPassword: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface CreateFolder extends Base {
 | 
				
			||||||
 | 
							parent: number;
 | 
				
			||||||
 | 
							name: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface CreateZip extends Base {
 | 
				
			||||||
 | 
							nodes: number[];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/no-namespace
 | 
				
			||||||
 | 
					export namespace Responses {
 | 
				
			||||||
 | 
						export interface Base {
 | 
				
			||||||
 | 
							statusCode: number;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface Success extends Base {
 | 
				
			||||||
 | 
							statusCode: 200;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface Error extends Base {
 | 
				
			||||||
 | 
							statusCode: 400 | 401 | 403;
 | 
				
			||||||
 | 
							message?: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface Login extends Success {
 | 
				
			||||||
 | 
							jwt: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface RequestsTotpTfa extends Success {
 | 
				
			||||||
 | 
							qrCode: string;
 | 
				
			||||||
 | 
							secret: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface GetRoot extends Success {
 | 
				
			||||||
 | 
							rootId: number;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface GetNodeEntry {
 | 
				
			||||||
 | 
							id: number;
 | 
				
			||||||
 | 
							name: string;
 | 
				
			||||||
 | 
							isFile: boolean;
 | 
				
			||||||
 | 
							preview: boolean;
 | 
				
			||||||
 | 
							parent: number | null;
 | 
				
			||||||
 | 
							size?: number;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface GetNode extends Success, GetNodeEntry {
 | 
				
			||||||
 | 
							children?: GetNodeEntry[];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface PathSegment {
 | 
				
			||||||
 | 
							path: string;
 | 
				
			||||||
 | 
							node?: number;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface GetPath extends Success {
 | 
				
			||||||
 | 
							segments: PathSegment[];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface CreateFolder extends Success {
 | 
				
			||||||
 | 
							id: number;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface CreateFolderExists extends Success {
 | 
				
			||||||
 | 
							exists: true;
 | 
				
			||||||
 | 
							id: number;
 | 
				
			||||||
 | 
							isFile: boolean;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface CreateZip extends Success {
 | 
				
			||||||
 | 
							done: boolean;
 | 
				
			||||||
 | 
							progress?: number;
 | 
				
			||||||
 | 
							total?: number;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface DownloadBase64 extends Success {
 | 
				
			||||||
 | 
							data: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface GetType extends Success {
 | 
				
			||||||
 | 
							type: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface UserInfo extends Success {
 | 
				
			||||||
 | 
							name: string;
 | 
				
			||||||
 | 
							gitlab: boolean;
 | 
				
			||||||
 | 
							tfaEnabled: boolean;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface GetUsersEntry {
 | 
				
			||||||
 | 
							id: number;
 | 
				
			||||||
 | 
							gitlab: boolean;
 | 
				
			||||||
 | 
							name: string;
 | 
				
			||||||
 | 
							role: UserRole;
 | 
				
			||||||
 | 
							tfaEnabled: boolean;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export interface GetUsers extends Success {
 | 
				
			||||||
 | 
							users: GetUsersEntry[];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +0,0 @@
 | 
				
			|||||||
import { BaseRequest } from "./base";
 | 
					 | 
				
			||||||
import { IsEnum, IsNumber } from "class-validator";
 | 
					 | 
				
			||||||
import { UserRole } from "@/dto";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class AdminRequest extends BaseRequest {
 | 
					 | 
				
			||||||
  @IsNumber()
 | 
					 | 
				
			||||||
  user: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class SetUserRole extends AdminRequest {
 | 
					 | 
				
			||||||
  @IsEnum(UserRole)
 | 
					 | 
				
			||||||
  role: UserRole;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class LogoutAll extends AdminRequest {}
 | 
					 | 
				
			||||||
export class DeleteUser extends AdminRequest {}
 | 
					 | 
				
			||||||
export class DisableTfa extends AdminRequest {}
 | 
					 | 
				
			||||||
@@ -1,50 +0,0 @@
 | 
				
			|||||||
import { BaseRequest } from "./base";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  IsBoolean,
 | 
					 | 
				
			||||||
  IsEmail,
 | 
					 | 
				
			||||||
  IsNotEmpty,
 | 
					 | 
				
			||||||
  IsOptional,
 | 
					 | 
				
			||||||
  IsString,
 | 
					 | 
				
			||||||
} from "class-validator";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class SignUpRequest extends BaseRequest {
 | 
					 | 
				
			||||||
  @IsEmail()
 | 
					 | 
				
			||||||
  username: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  password: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class LoginRequest extends SignUpRequest {
 | 
					 | 
				
			||||||
  @IsOptional()
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  otp?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class TfaSetup extends BaseRequest {
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsBoolean()
 | 
					 | 
				
			||||||
  mail: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class TfaComplete extends BaseRequest {
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsBoolean()
 | 
					 | 
				
			||||||
  mail: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  code: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class ChangePasswordRequest extends BaseRequest {
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  oldPassword: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  newPassword: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1 +0,0 @@
 | 
				
			|||||||
export class BaseRequest {}
 | 
					 | 
				
			||||||
@@ -1,14 +0,0 @@
 | 
				
			|||||||
import { BaseRequest } from "./base";
 | 
					 | 
				
			||||||
import { IsInt, IsNotEmpty, IsString, Min } from "class-validator";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class CreateFolderRequest extends BaseRequest {
 | 
					 | 
				
			||||||
  @IsInt()
 | 
					 | 
				
			||||||
  @Min(1)
 | 
					 | 
				
			||||||
  parent: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class CreateFileRequest extends CreateFolderRequest {}
 | 
					 | 
				
			||||||
@@ -1,4 +0,0 @@
 | 
				
			|||||||
export * from "./base";
 | 
					 | 
				
			||||||
export * as Auth from "./auth";
 | 
					 | 
				
			||||||
export * as FS from "./fs";
 | 
					 | 
				
			||||||
export * as Admin from "./admin";
 | 
					 | 
				
			||||||
@@ -1,61 +0,0 @@
 | 
				
			|||||||
import { SuccessResponse } from "./base";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  IsArray,
 | 
					 | 
				
			||||||
  IsBoolean,
 | 
					 | 
				
			||||||
  IsEnum,
 | 
					 | 
				
			||||||
  IsNotEmpty,
 | 
					 | 
				
			||||||
  IsNumber,
 | 
					 | 
				
			||||||
  IsString,
 | 
					 | 
				
			||||||
  ValidateNested,
 | 
					 | 
				
			||||||
} from "class-validator";
 | 
					 | 
				
			||||||
import { UserRole, ValidateConstructor } from "../utils";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ValidateConstructor
 | 
					 | 
				
			||||||
export class GetUsersEntry {
 | 
					 | 
				
			||||||
  constructor(
 | 
					 | 
				
			||||||
    id: number,
 | 
					 | 
				
			||||||
    gitlab: boolean,
 | 
					 | 
				
			||||||
    name: string,
 | 
					 | 
				
			||||||
    role: UserRole,
 | 
					 | 
				
			||||||
    tfaEnabled: boolean
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    this.id = id;
 | 
					 | 
				
			||||||
    this.gitlab = gitlab;
 | 
					 | 
				
			||||||
    this.name = name;
 | 
					 | 
				
			||||||
    this.role = role;
 | 
					 | 
				
			||||||
    this.tfaEnabled = tfaEnabled;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNumber()
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsBoolean()
 | 
					 | 
				
			||||||
  gitlab: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsEnum(UserRole)
 | 
					 | 
				
			||||||
  role: UserRole;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsBoolean()
 | 
					 | 
				
			||||||
  tfaEnabled: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ValidateConstructor
 | 
					 | 
				
			||||||
export class GetUsers extends SuccessResponse {
 | 
					 | 
				
			||||||
  constructor(users: GetUsersEntry[]) {
 | 
					 | 
				
			||||||
    super();
 | 
					 | 
				
			||||||
    this.users = users;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsArray()
 | 
					 | 
				
			||||||
  @ValidateNested({ each: true })
 | 
					 | 
				
			||||||
  users: GetUsersEntry[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class LogoutAllUser extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class DeleteUser extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class SetUserRole extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class DisableTfa extends SuccessResponse {}
 | 
					 | 
				
			||||||
@@ -1,40 +0,0 @@
 | 
				
			|||||||
import { SuccessResponse } from "./base";
 | 
					 | 
				
			||||||
import { IsBase32, IsJWT, IsNotEmpty } from "class-validator";
 | 
					 | 
				
			||||||
import { ValidateConstructor } from "../utils";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ValidateConstructor
 | 
					 | 
				
			||||||
export class LoginResponse extends SuccessResponse {
 | 
					 | 
				
			||||||
  constructor(jwt: string) {
 | 
					 | 
				
			||||||
    super();
 | 
					 | 
				
			||||||
    this.jwt = jwt;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsJWT()
 | 
					 | 
				
			||||||
  jwt: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ValidateConstructor
 | 
					 | 
				
			||||||
export class RequestTotpTfaResponse extends SuccessResponse {
 | 
					 | 
				
			||||||
  constructor(qrCode: string, secret: string) {
 | 
					 | 
				
			||||||
    super();
 | 
					 | 
				
			||||||
    this.qrCode = qrCode;
 | 
					 | 
				
			||||||
    this.secret = secret;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  qrCode: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsBase32()
 | 
					 | 
				
			||||||
  secret: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class TfaRequiredResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class RemoveTfaResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class RequestEmailTfaResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class TfaCompletedResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class SignupResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class ChangePasswordResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class LogoutAllResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class RefreshResponse extends LoginResponse {}
 | 
					 | 
				
			||||||
@@ -1,25 +0,0 @@
 | 
				
			|||||||
import { IsNumber, Max, Min } from "class-validator";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class BaseResponse {
 | 
					 | 
				
			||||||
  constructor(statusCode: number) {
 | 
					 | 
				
			||||||
    this.statusCode = statusCode;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNumber()
 | 
					 | 
				
			||||||
  @Min(100)
 | 
					 | 
				
			||||||
  @Max(599)
 | 
					 | 
				
			||||||
  statusCode: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class SuccessResponse extends BaseResponse {
 | 
					 | 
				
			||||||
  constructor() {
 | 
					 | 
				
			||||||
    super(200);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  declare statusCode: 200;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class ErrorResponse extends BaseResponse {
 | 
					 | 
				
			||||||
  declare statusCode: 400 | 401 | 403;
 | 
					 | 
				
			||||||
  message?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,89 +0,0 @@
 | 
				
			|||||||
import { SuccessResponse } from "./base";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  IsBoolean,
 | 
					 | 
				
			||||||
  IsInt,
 | 
					 | 
				
			||||||
  IsNotEmpty,
 | 
					 | 
				
			||||||
  IsOptional,
 | 
					 | 
				
			||||||
  IsString,
 | 
					 | 
				
			||||||
  Min,
 | 
					 | 
				
			||||||
} from "class-validator";
 | 
					 | 
				
			||||||
import { ValidateConstructor } from "../utils";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ValidateConstructor
 | 
					 | 
				
			||||||
export class GetRootResponse extends SuccessResponse {
 | 
					 | 
				
			||||||
  constructor(rootId: number) {
 | 
					 | 
				
			||||||
    super();
 | 
					 | 
				
			||||||
    this.rootId = rootId;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  @IsInt()
 | 
					 | 
				
			||||||
  @Min(1)
 | 
					 | 
				
			||||||
  rootId: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class GetNodeResponse extends SuccessResponse {
 | 
					 | 
				
			||||||
  constructor(
 | 
					 | 
				
			||||||
    id: number,
 | 
					 | 
				
			||||||
    name: string,
 | 
					 | 
				
			||||||
    isFile: boolean,
 | 
					 | 
				
			||||||
    parent: number | null
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    super();
 | 
					 | 
				
			||||||
    this.id = id;
 | 
					 | 
				
			||||||
    this.name = name;
 | 
					 | 
				
			||||||
    this.isFile = isFile;
 | 
					 | 
				
			||||||
    this.parent = parent;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsInt()
 | 
					 | 
				
			||||||
  @Min(1)
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsBoolean()
 | 
					 | 
				
			||||||
  isFile: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsOptional()
 | 
					 | 
				
			||||||
  @IsInt()
 | 
					 | 
				
			||||||
  @Min(1)
 | 
					 | 
				
			||||||
  parent: number | null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsOptional()
 | 
					 | 
				
			||||||
  @IsInt({ each: true })
 | 
					 | 
				
			||||||
  @Min(1, { each: true })
 | 
					 | 
				
			||||||
  children?: number[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsOptional()
 | 
					 | 
				
			||||||
  @IsInt()
 | 
					 | 
				
			||||||
  @Min(0)
 | 
					 | 
				
			||||||
  size?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ValidateConstructor
 | 
					 | 
				
			||||||
export class GetPathResponse extends SuccessResponse {
 | 
					 | 
				
			||||||
  constructor(path: string) {
 | 
					 | 
				
			||||||
    super();
 | 
					 | 
				
			||||||
    this.path = path;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  path: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ValidateConstructor
 | 
					 | 
				
			||||||
export class CreateFolderResponse extends SuccessResponse {
 | 
					 | 
				
			||||||
  constructor(id: number) {
 | 
					 | 
				
			||||||
    super();
 | 
					 | 
				
			||||||
    this.id = id;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsInt()
 | 
					 | 
				
			||||||
  @Min(1)
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class UploadFileResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class DeleteResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class CreateFileResponse extends CreateFolderResponse {}
 | 
					 | 
				
			||||||
@@ -1,5 +0,0 @@
 | 
				
			|||||||
export * from "./base";
 | 
					 | 
				
			||||||
export * as Auth from "./auth";
 | 
					 | 
				
			||||||
export * as FS from "./fs";
 | 
					 | 
				
			||||||
export * as User from "./user";
 | 
					 | 
				
			||||||
export * as Admin from "./admin";
 | 
					 | 
				
			||||||
@@ -1,27 +0,0 @@
 | 
				
			|||||||
import { SuccessResponse } from "./base";
 | 
					 | 
				
			||||||
import { ValidateConstructor } from "../utils";
 | 
					 | 
				
			||||||
import { IsBoolean, IsNotEmpty, IsString } from "class-validator";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ValidateConstructor
 | 
					 | 
				
			||||||
export class UserInfoResponse extends SuccessResponse {
 | 
					 | 
				
			||||||
  constructor(name: string, gitlab: boolean, tfaEnabled: boolean) {
 | 
					 | 
				
			||||||
    super();
 | 
					 | 
				
			||||||
    this.name = name;
 | 
					 | 
				
			||||||
    this.gitlab = gitlab;
 | 
					 | 
				
			||||||
    this.tfaEnabled = tfaEnabled;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsBoolean()
 | 
					 | 
				
			||||||
  gitlab: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsBoolean()
 | 
					 | 
				
			||||||
  tfaEnabled: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class DeleteUserResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class ChangePasswordResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
export class LogoutAllResponse extends SuccessResponse {}
 | 
					 | 
				
			||||||
@@ -1,43 +0,0 @@
 | 
				
			|||||||
import { validate, validateSync as _validateSync } from "class-validator";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export enum UserRole {
 | 
					 | 
				
			||||||
  ADMIN = 2,
 | 
					 | 
				
			||||||
  USER = 1,
 | 
					 | 
				
			||||||
  DISABLED = 0,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function validateSync<T extends object>(data: T): void {
 | 
					 | 
				
			||||||
  const errors = _validateSync(data);
 | 
					 | 
				
			||||||
  if (errors.length > 0) {
 | 
					 | 
				
			||||||
    console.error("Validation failed, errors: ", errors);
 | 
					 | 
				
			||||||
    throw new Error("Validation failed");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function validateAsync<T extends object>(data: T): Promise<void> {
 | 
					 | 
				
			||||||
  const errors = await validate(data);
 | 
					 | 
				
			||||||
  if (errors.length > 0) {
 | 
					 | 
				
			||||||
    console.error("Validation failed, errors: ", errors);
 | 
					 | 
				
			||||||
    throw new Error("Validation failed");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function validateAsyncInline<T extends object>(
 | 
					 | 
				
			||||||
  data: T
 | 
					 | 
				
			||||||
): Promise<T> {
 | 
					 | 
				
			||||||
  await validateAsync(data);
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function ValidateConstructor<
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
					 | 
				
			||||||
  T extends { new (...args: any[]): any }
 | 
					 | 
				
			||||||
>(constr: T) {
 | 
					 | 
				
			||||||
  return class extends constr {
 | 
					 | 
				
			||||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
					 | 
				
			||||||
    constructor(...args: any[]) {
 | 
					 | 
				
			||||||
      super(...args);
 | 
					 | 
				
			||||||
      validateSync(this);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
import { createApp } from "vue";
 | 
					import { createApp } from 'vue';
 | 
				
			||||||
import AppAsyncWrapper from "./AppAsyncWrapper.vue";
 | 
					import AppAsyncWrapper from './AppAsyncWrapper.vue';
 | 
				
			||||||
import router from "./router";
 | 
					import router from './router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const app = createApp(AppAsyncWrapper);
 | 
					const app = createApp(AppAsyncWrapper);
 | 
				
			||||||
app.use(router);
 | 
					app.use(router);
 | 
				
			||||||
app.config.unwrapInjectedRef = true;
 | 
					app.config.unwrapInjectedRef = true;
 | 
				
			||||||
app.mount("#app");
 | 
					app.mount('#app');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,64 +1,58 @@
 | 
				
			|||||||
import type { RouteRecordRaw } from "vue-router";
 | 
					import type { RouteRecordRaw } from 'vue-router';
 | 
				
			||||||
import { createRouter, createWebHistory } from "vue-router";
 | 
					import { createRouter, createWebHistory } from 'vue-router';
 | 
				
			||||||
import LoginView from "@/views/LoginView.vue";
 | 
					import LoginView from '@/views/LoginView.vue';
 | 
				
			||||||
import SignupView from "@/views/SignupView.vue";
 | 
					import SignupView from '@/views/SignupView.vue';
 | 
				
			||||||
import HomeView from "@/views/HomeView.vue";
 | 
					import HomeView from '@/views/HomeView.vue';
 | 
				
			||||||
import AboutView from "@/views/AboutView.vue";
 | 
					import FSView from '@/views/FSView.vue';
 | 
				
			||||||
import FSView from "@/views/FSView.vue";
 | 
					import SetTokenView from '@/views/SetTokenView.vue';
 | 
				
			||||||
import SetTokenView from "@/views/SetTokenView.vue";
 | 
					import ProfileView from '@/views/ProfileView.vue';
 | 
				
			||||||
import ProfileView from "@/views/ProfileView.vue";
 | 
					import TFAView from '@/views/TFAView.vue';
 | 
				
			||||||
import TFAView from "@/views/TFAView.vue";
 | 
					import AdminView from '@/views/AdminView.vue';
 | 
				
			||||||
import AdminView from "@/views/AdminView.vue";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const routes: Array<RouteRecordRaw> = [
 | 
					const routes: Array<RouteRecordRaw> = [
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    path: "/",
 | 
							path: '/',
 | 
				
			||||||
    name: "home",
 | 
							name: 'home',
 | 
				
			||||||
    component: HomeView,
 | 
							component: HomeView
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    path: "/profile",
 | 
							path: '/profile',
 | 
				
			||||||
    name: "profile",
 | 
							name: 'profile',
 | 
				
			||||||
    component: ProfileView,
 | 
							component: ProfileView
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    path: "/profile/2fa-enable",
 | 
							path: '/profile/2fa-enable',
 | 
				
			||||||
    name: "2fa",
 | 
							name: '2fa',
 | 
				
			||||||
    component: TFAView,
 | 
							component: TFAView
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    path: "/admin",
 | 
							path: '/admin',
 | 
				
			||||||
    component: AdminView,
 | 
							component: AdminView
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
  {
 | 
						{
 | 
				
			||||||
    path: "/about",
 | 
							path: '/login',
 | 
				
			||||||
    component: AboutView,
 | 
							name: 'login',
 | 
				
			||||||
  },
 | 
							component: LoginView
 | 
				
			||||||
  {
 | 
						},
 | 
				
			||||||
    path: "/login",
 | 
						{
 | 
				
			||||||
    name: "login",
 | 
							path: '/signup',
 | 
				
			||||||
    component: LoginView,
 | 
							name: 'signup',
 | 
				
			||||||
  },
 | 
							component: SignupView
 | 
				
			||||||
  {
 | 
						},
 | 
				
			||||||
    path: "/signup",
 | 
						{
 | 
				
			||||||
    name: "signup",
 | 
							path: '/fs/:node_id',
 | 
				
			||||||
    component: SignupView,
 | 
							name: 'fs',
 | 
				
			||||||
  },
 | 
							component: FSView
 | 
				
			||||||
  {
 | 
						},
 | 
				
			||||||
    path: "/fs/:node_id",
 | 
						{
 | 
				
			||||||
    name: "fs",
 | 
							path: '/set_token',
 | 
				
			||||||
    component: FSView,
 | 
							component: SetTokenView
 | 
				
			||||||
  },
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    path: "/set_token",
 | 
					 | 
				
			||||||
    component: SetTokenView,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = createRouter({
 | 
					const router = createRouter({
 | 
				
			||||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
						history: createWebHistory(import.meta.env.BASE_URL),
 | 
				
			||||||
  routes,
 | 
						routes
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default router;
 | 
					export default router;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										19
									
								
								frontend/src/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/utils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					import type { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
 | 
					export function loadingMsgWrapper<T extends (...args: any[]) => Promise<any>>(
 | 
				
			||||||
 | 
						msg: MessageApiInjection,
 | 
				
			||||||
 | 
						func: T
 | 
				
			||||||
 | 
					): T {
 | 
				
			||||||
 | 
						return <T>(async (...args: never[]) => {
 | 
				
			||||||
 | 
							const loadMsg = msg.loading('Working', {
 | 
				
			||||||
 | 
								duration: 0,
 | 
				
			||||||
 | 
								closable: false
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								return await func(...args);
 | 
				
			||||||
 | 
							} finally {
 | 
				
			||||||
 | 
								loadMsg.destroy();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
  <div class="about">
 | 
					 | 
				
			||||||
    <h1>This is an about page</h1>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
@@ -1,104 +1,152 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="tsx">
 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					import type { TokenInjectType, Responses } from '@/api';
 | 
				
			||||||
import { inject, onBeforeMount, ref } from "vue";
 | 
					import type { SelectOption, DataTableColumn } from 'naive-ui';
 | 
				
			||||||
import { Responses, check_token, Admin, isErrorResponse } from "@/api";
 | 
					import { inject, onBeforeMount, ref } from 'vue';
 | 
				
			||||||
import { onBeforeRouteUpdate } from "vue-router";
 | 
					import { check_token, Admin, isErrorResponse } from '@/api';
 | 
				
			||||||
import router from "@/router";
 | 
					import { onBeforeRouteUpdate } from 'vue-router';
 | 
				
			||||||
 | 
					import router from '@/router';
 | 
				
			||||||
 | 
					import { loadingMsgWrapper } from '@/utils';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						useMessage,
 | 
				
			||||||
 | 
						NDataTable,
 | 
				
			||||||
 | 
						NSelect,
 | 
				
			||||||
 | 
						NButton,
 | 
				
			||||||
 | 
						NButtonGroup
 | 
				
			||||||
 | 
					} from 'naive-ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					const message = useMessage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const users = ref<Responses.Admin.GetUsersEntry[]>([]);
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const users = ref<Responses.GetUsersEntry[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onBeforeRouteUpdate(async () => {
 | 
					onBeforeRouteUpdate(async () => {
 | 
				
			||||||
  await updatePanel();
 | 
						await updatePanel();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
onBeforeMount(async () => {
 | 
					onBeforeMount(async () => {
 | 
				
			||||||
  await updatePanel();
 | 
						await updatePanel();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
async function updatePanel() {
 | 
					 | 
				
			||||||
  const token = await check_token(jwt);
 | 
					 | 
				
			||||||
  if (!token) return;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const res = await Admin.get_users(token);
 | 
					const updatePanel = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
  if (isErrorResponse(res)) return router.replace({ path: "/" });
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  users.value = res.users;
 | 
						if (!token) return;
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function setRole(user: number, roleStr: string) {
 | 
						const res = await Admin.get_users(token);
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						if (isErrorResponse(res)) return router.replace({ path: '/' });
 | 
				
			||||||
  if (!token) return;
 | 
						users.value = res.users;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const res = await Admin.set_role(user, parseInt(roleStr, 10), token);
 | 
					const setRole = loadingMsgWrapper(
 | 
				
			||||||
  if (isErrorResponse(res)) console.error(res.message);
 | 
						message,
 | 
				
			||||||
  await updatePanel();
 | 
						async (user: number, role: number) => {
 | 
				
			||||||
}
 | 
							const token = await check_token(jwt);
 | 
				
			||||||
 | 
							if (!token) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function disableTfa(user: number) {
 | 
							const res = await Admin.set_role(user, role, token);
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
							if (isErrorResponse(res)) console.error(res.message);
 | 
				
			||||||
  if (!token) return;
 | 
							await updatePanel();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const res = await Admin.disable_tfa(user, token);
 | 
					const action = (
 | 
				
			||||||
  if (isErrorResponse(res)) console.error(res.message);
 | 
						func: (
 | 
				
			||||||
  await updatePanel();
 | 
							user: number,
 | 
				
			||||||
}
 | 
							token: string
 | 
				
			||||||
 | 
						) => Promise<Responses.Success | Responses.Error>
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
						return loadingMsgWrapper(message, async (user: number) => {
 | 
				
			||||||
 | 
							const token = await check_token(jwt);
 | 
				
			||||||
 | 
							if (!token) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function logoutUser(user: number) {
 | 
							const res = await func(user, token);
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
							if (isErrorResponse(res)) console.error(res.message);
 | 
				
			||||||
  if (!token) return;
 | 
							await updatePanel();
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const res = await Admin.logout(user, token);
 | 
					const logoutUser = action(Admin.logout);
 | 
				
			||||||
  if (isErrorResponse(res)) console.error(res.message);
 | 
					const disableTfa = action(Admin.disable_tfa);
 | 
				
			||||||
  await updatePanel();
 | 
					const deleteUser = action(Admin.delete_user);
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function deleteUser(user: number) {
 | 
					const selectOptions: SelectOption[] = [
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						{
 | 
				
			||||||
  if (!token) return;
 | 
							label: 'Disabled',
 | 
				
			||||||
 | 
							value: 0
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							label: 'User',
 | 
				
			||||||
 | 
							value: 1
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							label: 'Admin',
 | 
				
			||||||
 | 
							value: 2
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const res = await Admin.delete_user(user, token);
 | 
					const columns: DataTableColumn<Responses.GetUsersEntry>[] = [
 | 
				
			||||||
  if (isErrorResponse(res)) console.error(res.message);
 | 
						{
 | 
				
			||||||
  await updatePanel();
 | 
							title: 'Name',
 | 
				
			||||||
}
 | 
							key: 'name'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							title: 'Type',
 | 
				
			||||||
 | 
							key: 'gitlab',
 | 
				
			||||||
 | 
							render(user) {
 | 
				
			||||||
 | 
								return user.gitlab ? 'Gitlab' : 'Password';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							title: 'Role',
 | 
				
			||||||
 | 
							key: 'role',
 | 
				
			||||||
 | 
							minWidth: 120,
 | 
				
			||||||
 | 
							render(user) {
 | 
				
			||||||
 | 
								return (
 | 
				
			||||||
 | 
									<NSelect
 | 
				
			||||||
 | 
										value={user.role}
 | 
				
			||||||
 | 
										options={selectOptions}
 | 
				
			||||||
 | 
										onUpdateValue={(value: number) => setRole(user.id, value)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							title: 'Tfa Status',
 | 
				
			||||||
 | 
							key: 'tfaEnabled',
 | 
				
			||||||
 | 
							render(user) {
 | 
				
			||||||
 | 
								return user.gitlab ? '' : user.tfaEnabled ? 'Enabled' : 'Disabled';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							title: 'Actions',
 | 
				
			||||||
 | 
							key: 'actions',
 | 
				
			||||||
 | 
							render(user) {
 | 
				
			||||||
 | 
								return (
 | 
				
			||||||
 | 
									<NButtonGroup>
 | 
				
			||||||
 | 
										<NButton onClick={() => logoutUser(user.id)}>
 | 
				
			||||||
 | 
											Logout all
 | 
				
			||||||
 | 
										</NButton>
 | 
				
			||||||
 | 
										{user.tfaEnabled ? (
 | 
				
			||||||
 | 
											<NButton
 | 
				
			||||||
 | 
												type="warning"
 | 
				
			||||||
 | 
												onClick={() => disableTfa(user.id)}
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
												Disable Tfa
 | 
				
			||||||
 | 
											</NButton>
 | 
				
			||||||
 | 
										) : (
 | 
				
			||||||
 | 
											''
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
										<NButton onClick={() => deleteUser(user.id)} type="error">
 | 
				
			||||||
 | 
											Delete
 | 
				
			||||||
 | 
										</NButton>
 | 
				
			||||||
 | 
									</NButtonGroup>
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <table>
 | 
						<n-data-table :columns="columns" :data="users" />
 | 
				
			||||||
    <tr>
 | 
					 | 
				
			||||||
      <th>Name</th>
 | 
					 | 
				
			||||||
      <th>Type</th>
 | 
					 | 
				
			||||||
      <th>Role</th>
 | 
					 | 
				
			||||||
      <th>Tfa Status</th>
 | 
					 | 
				
			||||||
      <th>Actions</th>
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
    <tr v-for="user in users" :key="user.id">
 | 
					 | 
				
			||||||
      <td>{{ user.name }}</td>
 | 
					 | 
				
			||||||
      <td>{{ user.gitlab ? "Gitlab" : "Password" }}</td>
 | 
					 | 
				
			||||||
      <td>
 | 
					 | 
				
			||||||
        <select @change="setRole(user.id, ($event.target as HTMLSelectElement).value)">
 | 
					 | 
				
			||||||
          <option value="0" :selected="user.role === 0 ? true : undefined">
 | 
					 | 
				
			||||||
            Disabled
 | 
					 | 
				
			||||||
          </option>
 | 
					 | 
				
			||||||
          <option value="1" :selected="user.role === 1 ? true : undefined">
 | 
					 | 
				
			||||||
            User
 | 
					 | 
				
			||||||
          </option>
 | 
					 | 
				
			||||||
          <option value="2" :selected="user.role === 2 ? true : undefined">
 | 
					 | 
				
			||||||
            Admin
 | 
					 | 
				
			||||||
          </option>
 | 
					 | 
				
			||||||
        </select>
 | 
					 | 
				
			||||||
      </td>
 | 
					 | 
				
			||||||
      <td v-if="user.gitlab"></td>
 | 
					 | 
				
			||||||
      <td v-else>
 | 
					 | 
				
			||||||
        {{ user.tfaEnabled ? "Enabled" : "Disabled" }}
 | 
					 | 
				
			||||||
      </td>
 | 
					 | 
				
			||||||
      <td>
 | 
					 | 
				
			||||||
        <button v-if="user.tfaEnabled" @click="disableTfa(user.id)">
 | 
					 | 
				
			||||||
          Disable Tfa
 | 
					 | 
				
			||||||
        </button>
 | 
					 | 
				
			||||||
        <button @click="logoutUser(user.id)">Logout all</button>
 | 
					 | 
				
			||||||
        <button @click="deleteUser(user.id)">Delete</button>
 | 
					 | 
				
			||||||
      </td>
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
  </table>
 | 
					 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped></style>
 | 
					<style scoped></style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,65 +1,92 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					import type { TokenInjectType, Responses } from '@/api';
 | 
				
			||||||
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
 | 
					import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
 | 
				
			||||||
import { inject, onBeforeMount, ref } from "vue";
 | 
					import { inject, onBeforeMount, ref } from 'vue';
 | 
				
			||||||
import { check_token, FS, Responses, isErrorResponse } from "@/api";
 | 
					import { NCard } from 'naive-ui';
 | 
				
			||||||
import DirViewer from "@/components/FSView/DirViewer.vue";
 | 
					import { check_token, FS, isErrorResponse } from '@/api';
 | 
				
			||||||
import FileViewer from "@/components/FSView/FileViewer.vue";
 | 
					import UploadField from '@/components/UploadDialog/UploadField.vue';
 | 
				
			||||||
 | 
					import DirViewer from '@/components/DirViewer/DirViewer.vue';
 | 
				
			||||||
 | 
					import FileViewer from '@/components/FileViewer/FileViewer.vue';
 | 
				
			||||||
 | 
					import NLink from '@/components/NLink.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = useRouter();
 | 
					const router = useRouter();
 | 
				
			||||||
const route = useRoute();
 | 
					const route = useRoute();
 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const path = ref("");
 | 
					const path = ref<Responses.GetPath | null>(null);
 | 
				
			||||||
const node = ref<Responses.FS.GetNodeResponse | null>(null);
 | 
					const node = ref<Responses.GetNode | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function nameCompare(a: Responses.GetNodeEntry, b: Responses.GetNodeEntry) {
 | 
				
			||||||
 | 
						const aStr = a.name.toLowerCase();
 | 
				
			||||||
 | 
						const bStr = b.name.toLowerCase();
 | 
				
			||||||
 | 
						return aStr.localeCompare(bStr);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function fetch_node(node_id: number) {
 | 
					async function fetch_node(node_id: number) {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  if (!token) return;
 | 
						if (!token) return;
 | 
				
			||||||
  const [p, n] = [
 | 
						const [p, n] = [
 | 
				
			||||||
    await FS.get_path(token, node_id),
 | 
							await FS.get_path(token, node_id),
 | 
				
			||||||
    await FS.get_node(token, node_id),
 | 
							await FS.get_node(token, node_id)
 | 
				
			||||||
  ];
 | 
						];
 | 
				
			||||||
  if (isErrorResponse(p)) return gotoRoot();
 | 
						if (isErrorResponse(p)) return gotoRoot();
 | 
				
			||||||
  if (isErrorResponse(n)) return gotoRoot();
 | 
						if (isErrorResponse(n)) return gotoRoot();
 | 
				
			||||||
  [path.value, node.value] = [p.path, n];
 | 
						if (n.children) {
 | 
				
			||||||
 | 
							const folders = n.children
 | 
				
			||||||
 | 
								.filter((node) => !node.isFile)
 | 
				
			||||||
 | 
								.sort(nameCompare);
 | 
				
			||||||
 | 
							const files = n.children
 | 
				
			||||||
 | 
								.filter((node) => node.isFile)
 | 
				
			||||||
 | 
								.sort(nameCompare);
 | 
				
			||||||
 | 
							n.children = [...folders, ...files];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						[path.value, node.value] = [p, n];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onBeforeRouteUpdate(async (to) => {
 | 
					onBeforeRouteUpdate(async (to) => {
 | 
				
			||||||
  await fetch_node(Number(to.params.node_id));
 | 
						await fetch_node(Number(to.params.node_id));
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function reloadNode() {
 | 
					async function reloadNode() {
 | 
				
			||||||
  await fetch_node(Number(route.params.node_id));
 | 
						await fetch_node(Number(route.params.node_id));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
onBeforeMount(async () => {
 | 
					onBeforeMount(async () => {
 | 
				
			||||||
  await reloadNode();
 | 
						await reloadNode();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function gotoRoot() {
 | 
					async function gotoRoot() {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  if (!token) return;
 | 
						if (!token) return;
 | 
				
			||||||
  const rootRes = await FS.get_root(token);
 | 
						const rootRes = await FS.get_root(token);
 | 
				
			||||||
  if (isErrorResponse(rootRes)) return jwt.logout();
 | 
						if (isErrorResponse(rootRes)) return jwt.logout();
 | 
				
			||||||
  const root = rootRes.rootId;
 | 
						const root = rootRes.rootId;
 | 
				
			||||||
  await router.replace({
 | 
						await router.replace({
 | 
				
			||||||
    name: "fs",
 | 
							name: 'fs',
 | 
				
			||||||
    params: { node_id: root },
 | 
							params: { node_id: root }
 | 
				
			||||||
  });
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div v-if="node">
 | 
						<n-card v-if="node" header-style="font-size: 1.5em">
 | 
				
			||||||
    <div>Path: {{ path }}</div>
 | 
							<template #header>
 | 
				
			||||||
    <DirViewer
 | 
								<span
 | 
				
			||||||
      v-if="!node.isFile"
 | 
									v-for="seg in path?.segments ?? []"
 | 
				
			||||||
      :node="node"
 | 
									:key="seg.path"
 | 
				
			||||||
      @reloadNode="reloadNode"
 | 
									style="margin-left: 0.25em"
 | 
				
			||||||
      @gotoRoot="gotoRoot"
 | 
								>
 | 
				
			||||||
    />
 | 
									<NLink v-if="seg.node" :to="`/fs/${seg.node}`">
 | 
				
			||||||
    <FileViewer v-else :node="node" />
 | 
										{{ seg.path }}
 | 
				
			||||||
  </div>
 | 
									</NLink>
 | 
				
			||||||
 | 
									<template v-else>{{ seg.path }}</template>
 | 
				
			||||||
 | 
								</span>
 | 
				
			||||||
 | 
							</template>
 | 
				
			||||||
 | 
							<template v-if="!node.isFile" #header-extra>
 | 
				
			||||||
 | 
								<UploadField :node="node" @reloadNode="reloadNode" />
 | 
				
			||||||
 | 
							</template>
 | 
				
			||||||
 | 
							<DirViewer v-if="!node.isFile" :node="node" @reloadNode="reloadNode" />
 | 
				
			||||||
 | 
							<FileViewer v-else :node="node" />
 | 
				
			||||||
 | 
						</n-card>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped></style>
 | 
					<style scoped></style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,29 +1,29 @@
 | 
				
			|||||||
<template><p></p></template>
 | 
					<template><p></p></template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					import type { TokenInjectType } from '@/api';
 | 
				
			||||||
import { onBeforeRouteUpdate, useRouter } from "vue-router";
 | 
					import { onBeforeRouteUpdate, useRouter } from 'vue-router';
 | 
				
			||||||
import { inject, onBeforeMount } from "vue";
 | 
					import { inject, onBeforeMount } from 'vue';
 | 
				
			||||||
import { FS, check_token, isErrorResponse } from "@/api";
 | 
					import { FS, check_token, isErrorResponse } from '@/api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = useRouter();
 | 
					const router = useRouter();
 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function start_redirect() {
 | 
					async function start_redirect() {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  if (!token) return;
 | 
						if (!token) return;
 | 
				
			||||||
  const root = await FS.get_root(token);
 | 
						const root = await FS.get_root(token);
 | 
				
			||||||
  if (isErrorResponse(root)) return jwt.logout();
 | 
						if (isErrorResponse(root)) return jwt.logout();
 | 
				
			||||||
  await router.replace({
 | 
						await router.replace({
 | 
				
			||||||
    name: "fs",
 | 
							name: 'fs',
 | 
				
			||||||
    params: { node_id: root.rootId },
 | 
							params: { node_id: root.rootId }
 | 
				
			||||||
  });
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onBeforeRouteUpdate(async () => {
 | 
					onBeforeRouteUpdate(async () => {
 | 
				
			||||||
  await start_redirect();
 | 
						await start_redirect();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
onBeforeMount(async () => {
 | 
					onBeforeMount(async () => {
 | 
				
			||||||
  await start_redirect();
 | 
						await start_redirect();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,62 +1,142 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					import type { TokenInjectType } from '@/api';
 | 
				
			||||||
import { ref, inject } from "vue";
 | 
					import { ref, inject } from 'vue';
 | 
				
			||||||
import { Auth, FS, isErrorResponse } from "@/api";
 | 
					import { Auth, FS, isErrorResponse } from '@/api';
 | 
				
			||||||
import { useRouter } from "vue-router";
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						useMessage,
 | 
				
			||||||
 | 
						NInput,
 | 
				
			||||||
 | 
						NGrid,
 | 
				
			||||||
 | 
						NGi,
 | 
				
			||||||
 | 
						NButton,
 | 
				
			||||||
 | 
						NIcon,
 | 
				
			||||||
 | 
						NH4,
 | 
				
			||||||
 | 
						NCard
 | 
				
			||||||
 | 
					} from 'naive-ui';
 | 
				
			||||||
 | 
					import { LogoGitlab } from '@vicons/ionicons5';
 | 
				
			||||||
 | 
					import { loadingMsgWrapper } from '@/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = useRouter();
 | 
					const router = useRouter();
 | 
				
			||||||
 | 
					const message = useMessage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const username = ref("");
 | 
					const username = ref('');
 | 
				
			||||||
const password = ref("");
 | 
					const password = ref('');
 | 
				
			||||||
const otp = ref("");
 | 
					const otp = ref('');
 | 
				
			||||||
 | 
					 | 
				
			||||||
const error = ref("");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const requestOtp = ref(false);
 | 
					const requestOtp = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function login() {
 | 
					const login = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
  error.value = "";
 | 
						if (username.value === '' || password.value === '') {
 | 
				
			||||||
  if (username.value === "" || password.value === "") {
 | 
							message.error('Email and/or Password missing', {
 | 
				
			||||||
    error.value = "Email and/or Password missing";
 | 
								closable: true,
 | 
				
			||||||
    return;
 | 
								duration: 5000
 | 
				
			||||||
  }
 | 
							});
 | 
				
			||||||
  const res = await (requestOtp.value
 | 
							return;
 | 
				
			||||||
    ? Auth.auth_login(username.value, password.value, otp.value)
 | 
						}
 | 
				
			||||||
    : Auth.auth_login(username.value, password.value));
 | 
						const res = await (requestOtp.value
 | 
				
			||||||
  if (isErrorResponse(res)) error.value = "Login failed: " + res.message;
 | 
							? Auth.auth_login(username.value, password.value, otp.value)
 | 
				
			||||||
  else if ("jwt" in res) {
 | 
							: Auth.auth_login(username.value, password.value));
 | 
				
			||||||
    const root = await FS.get_root(res.jwt);
 | 
						if (isErrorResponse(res)) {
 | 
				
			||||||
    if (isErrorResponse(root)) {
 | 
							message.error(`Login failed: ${res.message}`, {
 | 
				
			||||||
      error.value = "Get root failed: " + root.message;
 | 
								closable: true,
 | 
				
			||||||
      return;
 | 
								duration: 5000
 | 
				
			||||||
    }
 | 
							});
 | 
				
			||||||
    jwt.setToken(res.jwt);
 | 
						} else if ('jwt' in res) {
 | 
				
			||||||
    await router.push({
 | 
							const root = await FS.get_root(res.jwt);
 | 
				
			||||||
      name: "fs",
 | 
							if (isErrorResponse(root)) {
 | 
				
			||||||
      params: { node_id: root.rootId },
 | 
								message.error(`Get root failed: ${root.message}`, {
 | 
				
			||||||
    });
 | 
									closable: true,
 | 
				
			||||||
  } else {
 | 
									duration: 5000
 | 
				
			||||||
    error.value = "";
 | 
								});
 | 
				
			||||||
    requestOtp.value = true;
 | 
								return;
 | 
				
			||||||
  }
 | 
							}
 | 
				
			||||||
 | 
							jwt.setToken(res.jwt);
 | 
				
			||||||
 | 
							await router.push({
 | 
				
			||||||
 | 
								name: 'fs',
 | 
				
			||||||
 | 
								params: { node_id: root.rootId }
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						} else requestOtp.value = true;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loginGitlab() {
 | 
				
			||||||
 | 
						window.location.pathname = '/api/auth/gitlab';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function signup() {
 | 
				
			||||||
 | 
						router.replace('signup');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onKey(event: KeyboardEvent) {
 | 
				
			||||||
 | 
						if (event.key == 'Enter') login();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div v-if="error !== ''" v-text="error"></div>
 | 
						<n-card>
 | 
				
			||||||
  <template v-if="!requestOtp">
 | 
							<template v-if="!requestOtp">
 | 
				
			||||||
    <input type="email" placeholder="Email" v-model="username" />
 | 
								<n-grid cols="2" x-gap="16" y-gap="16">
 | 
				
			||||||
    <input type="password" placeholder="Password" v-model="password" />
 | 
									<n-gi span="2">
 | 
				
			||||||
    <a href="/api/auth/gitlab">Login with gitlab</a>
 | 
										<n-input
 | 
				
			||||||
    <router-link to="signup">Signup instead?</router-link>
 | 
											type="text"
 | 
				
			||||||
  </template>
 | 
											placeholder="Email"
 | 
				
			||||||
  <template v-else>
 | 
											v-model:value="username"
 | 
				
			||||||
    <div>Please input your 2 factor authentication code</div>
 | 
											autofocus
 | 
				
			||||||
    <input type="text" placeholder="Code" v-model="otp" />
 | 
											:input-props="{ type: 'email' }"
 | 
				
			||||||
  </template>
 | 
											@keyup="onKey"
 | 
				
			||||||
  <button @click="login()">Login</button>
 | 
										/>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi span="2">
 | 
				
			||||||
 | 
										<n-input
 | 
				
			||||||
 | 
											type="password"
 | 
				
			||||||
 | 
											placeholder="Password"
 | 
				
			||||||
 | 
											v-model:value="password"
 | 
				
			||||||
 | 
											@keyup="onKey"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi span="2" style="text-align: center">
 | 
				
			||||||
 | 
										<n-button type="info" @click="login">Login</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi>
 | 
				
			||||||
 | 
										<n-button
 | 
				
			||||||
 | 
											ghost
 | 
				
			||||||
 | 
											color="#fc6d27"
 | 
				
			||||||
 | 
											text-color="#000"
 | 
				
			||||||
 | 
											@click="loginGitlab"
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											<template #icon>
 | 
				
			||||||
 | 
												<n-icon color="#fc6d27"><LogoGitlab /></n-icon>
 | 
				
			||||||
 | 
											</template>
 | 
				
			||||||
 | 
											Login with gitlab
 | 
				
			||||||
 | 
										</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi style="text-align: right">
 | 
				
			||||||
 | 
										<n-button ghost @click="signup">Signup</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
								</n-grid>
 | 
				
			||||||
 | 
							</template>
 | 
				
			||||||
 | 
							<template v-else>
 | 
				
			||||||
 | 
								<n-grid cols="2" x-gap="16" y-gap="16">
 | 
				
			||||||
 | 
									<n-gi span="2" style="text-align: center">
 | 
				
			||||||
 | 
										<n-h4>Please input your 2 factor authentication code</n-h4>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi span="1">
 | 
				
			||||||
 | 
										<n-input
 | 
				
			||||||
 | 
											type="text"
 | 
				
			||||||
 | 
											placeholder="Code"
 | 
				
			||||||
 | 
											maxlength="6"
 | 
				
			||||||
 | 
											v-model:value="otp"
 | 
				
			||||||
 | 
											autofocus
 | 
				
			||||||
 | 
											@keyup="onKey"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi span="1" style="text-align: right">
 | 
				
			||||||
 | 
										<n-button type="info" @click="login">Login</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
								</n-grid>
 | 
				
			||||||
 | 
							</template>
 | 
				
			||||||
 | 
						</n-card>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped></style>
 | 
					<style scoped></style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,106 +1,108 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					import type { TokenInjectType, Responses } from '@/api';
 | 
				
			||||||
import { ref, inject, onBeforeMount } from "vue";
 | 
					import { ref, inject, onBeforeMount } from 'vue';
 | 
				
			||||||
import { Auth, User, check_token, isErrorResponse, Responses } from "@/api";
 | 
					import { Auth, User, check_token, isErrorResponse } from '@/api';
 | 
				
			||||||
import { onBeforeRouteUpdate } from "vue-router";
 | 
					import { onBeforeRouteUpdate, useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					import { NSpin, NGrid, NGi, NButton, NCard, useMessage } from 'naive-ui';
 | 
				
			||||||
 | 
					import UserChangePw from '@/components/UserChangePw.vue';
 | 
				
			||||||
 | 
					import { loadingMsgWrapper } from '@/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const error = ref("");
 | 
					const router = useRouter();
 | 
				
			||||||
const oldPw = ref("");
 | 
					const message = useMessage();
 | 
				
			||||||
const newPw = ref("");
 | 
					 | 
				
			||||||
const newPw2 = ref("");
 | 
					 | 
				
			||||||
const user = ref<Responses.User.UserInfoResponse | null>(null);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					const user = ref<Responses.UserInfo | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onBeforeRouteUpdate(async () => {
 | 
					onBeforeRouteUpdate(async () => {
 | 
				
			||||||
  await updateProfile();
 | 
						await updateProfile();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
onBeforeMount(async () => {
 | 
					onBeforeMount(async () => {
 | 
				
			||||||
  await updateProfile();
 | 
						await updateProfile();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
async function updateProfile() {
 | 
					const updateProfile = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  if (!token) return;
 | 
						if (!token) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const res = await User.get_user_info(token);
 | 
						const res = await User.get_user_info(token);
 | 
				
			||||||
  if (isErrorResponse(res)) return jwt.logout();
 | 
						if (isErrorResponse(res)) return jwt.logout();
 | 
				
			||||||
  user.value = res;
 | 
						user.value = res;
 | 
				
			||||||
}
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function deleteUser() {
 | 
					const deleteUser = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  if (!token) return;
 | 
						if (!token) return;
 | 
				
			||||||
  await User.delete_user(token);
 | 
						await User.delete_user(token);
 | 
				
			||||||
  jwt.logout();
 | 
						jwt.logout();
 | 
				
			||||||
}
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function logoutAll() {
 | 
					const logoutAll = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  if (!token) return;
 | 
						if (!token) return;
 | 
				
			||||||
  await Auth.logout_all(token);
 | 
						await Auth.logout_all(token);
 | 
				
			||||||
  jwt.logout();
 | 
						jwt.logout();
 | 
				
			||||||
}
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function changePw() {
 | 
					const tfaDisable = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
  if (oldPw.value === "" || newPw.value === "" || newPw2.value === "") {
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
    error.value = "Password missing";
 | 
						if (!token) return;
 | 
				
			||||||
    return;
 | 
						await Auth.tfa_disable(token);
 | 
				
			||||||
  }
 | 
						jwt.logout();
 | 
				
			||||||
  if (newPw.value !== newPw2.value) {
 | 
					});
 | 
				
			||||||
    error.value = "Passwords don't match";
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const token = await check_token(jwt);
 | 
					 | 
				
			||||||
  if (!token) return;
 | 
					 | 
				
			||||||
  const res = await Auth.change_password(oldPw.value, newPw.value, token);
 | 
					 | 
				
			||||||
  if (isErrorResponse(res))
 | 
					 | 
				
			||||||
    error.value = "Password change failed: " + res.message;
 | 
					 | 
				
			||||||
  else jwt.logout();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function tfaDisable() {
 | 
					async function tfaEnable() {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						await router.push('/profile/2fa-enable');
 | 
				
			||||||
  if (!token) return;
 | 
					 | 
				
			||||||
  await Auth.tfa_disable(token);
 | 
					 | 
				
			||||||
  jwt.logout();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <template v-if="user">
 | 
						<template v-if="user">
 | 
				
			||||||
    <div v-if="error !== ''" v-text="error"></div>
 | 
							<n-card :title="user.name">
 | 
				
			||||||
    <div>User: {{ user.name }}</div>
 | 
								<n-grid cols="2" x-gap="16" y-gap="16">
 | 
				
			||||||
    <div>Signed in with {{ user.gitlab ? "gitlab" : "password" }}</div>
 | 
									<template v-if="!user.gitlab">
 | 
				
			||||||
    <template v-if="!user.gitlab">
 | 
										<n-gi span="2">
 | 
				
			||||||
      <div>
 | 
											<n-grid cols="2" x-gap="16">
 | 
				
			||||||
        <input type="password" placeholder="Old password" v-model="oldPw" />
 | 
												<n-gi><UserChangePw /></n-gi>
 | 
				
			||||||
        <input type="password" placeholder="New password" v-model="newPw" />
 | 
												<n-gi>
 | 
				
			||||||
        <input
 | 
													<n-card
 | 
				
			||||||
          type="password"
 | 
														title="2 Factor authentication"
 | 
				
			||||||
          placeholder="Repeat new password"
 | 
														embedded
 | 
				
			||||||
          v-model="newPw2"
 | 
													>
 | 
				
			||||||
        />
 | 
														<n-button
 | 
				
			||||||
        <button @click="changePw">Change</button>
 | 
															v-if="user.tfaEnabled"
 | 
				
			||||||
      </div>
 | 
															type="error"
 | 
				
			||||||
      <div>
 | 
															@click="tfaDisable"
 | 
				
			||||||
        <div>
 | 
														>
 | 
				
			||||||
          2 Factor authentication:
 | 
															Disable
 | 
				
			||||||
          {{ user.tfaEnabled ? "Enabled" : "Disabled" }}
 | 
														</n-button>
 | 
				
			||||||
        </div>
 | 
														<n-button
 | 
				
			||||||
        <div>
 | 
															v-else
 | 
				
			||||||
          <a href="#" v-if="user.tfaEnabled" @click="tfaDisable"> Disable </a>
 | 
															type="success"
 | 
				
			||||||
          <router-link to="/profile/2fa-enable" v-else> Enable </router-link>
 | 
															@click="tfaEnable"
 | 
				
			||||||
        </div>
 | 
														>
 | 
				
			||||||
      </div>
 | 
															Enable
 | 
				
			||||||
    </template>
 | 
														</n-button>
 | 
				
			||||||
    <div>
 | 
													</n-card>
 | 
				
			||||||
      <a href="#" @click="logoutAll">Logout everywhere</a>
 | 
												</n-gi>
 | 
				
			||||||
      <a href="#" @click="deleteUser">Delete Account</a>
 | 
											</n-grid>
 | 
				
			||||||
    </div>
 | 
										</n-gi>
 | 
				
			||||||
  </template>
 | 
									</template>
 | 
				
			||||||
  <template v-else>
 | 
									<n-gi>
 | 
				
			||||||
    <div>Loading...</div>
 | 
										<n-button type="error" @click="logoutAll">
 | 
				
			||||||
  </template>
 | 
											Logout everywhere
 | 
				
			||||||
 | 
										</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi>
 | 
				
			||||||
 | 
										<n-button type="error" @click="deleteUser">
 | 
				
			||||||
 | 
											Delete Account
 | 
				
			||||||
 | 
										</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
								</n-grid>
 | 
				
			||||||
 | 
							</n-card>
 | 
				
			||||||
 | 
						</template>
 | 
				
			||||||
 | 
						<template v-else>
 | 
				
			||||||
 | 
							<div><n-spin size="small" />Loading...</div>
 | 
				
			||||||
 | 
						</template>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped></style>
 | 
					<style scoped></style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,19 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					import type { TokenInjectType } from '@/api';
 | 
				
			||||||
import { inject } from "vue";
 | 
					import { inject } from 'vue';
 | 
				
			||||||
import { useRoute, useRouter } from "vue-router";
 | 
					import { useRoute, useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = useRouter();
 | 
					const router = useRouter();
 | 
				
			||||||
const route = useRoute();
 | 
					const route = useRoute();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if ("token" in route.query) jwt.setToken(route.query["token"] as string);
 | 
					if ('token' in route.query) jwt.setToken(route.query['token'] as string);
 | 
				
			||||||
router.replace({ path: "/" });
 | 
					router.replace({ path: '/' });
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <router-link to="/">Click here to go home</router-link>
 | 
						<router-link to="/" replace>Click here to go home</router-link>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped></style>
 | 
					<style scoped></style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,35 +1,85 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { ref } from "vue";
 | 
					import { ref } from 'vue';
 | 
				
			||||||
import { Auth, isErrorResponse } from "@/api";
 | 
					import { Auth, isErrorResponse } from '@/api';
 | 
				
			||||||
 | 
					import { useMessage, NInput, NGrid, NGi, NButton, NCard } from 'naive-ui';
 | 
				
			||||||
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					import { loadingMsgWrapper } from '@/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const username = ref("");
 | 
					const router = useRouter();
 | 
				
			||||||
const password = ref("");
 | 
					const message = useMessage();
 | 
				
			||||||
const password2 = ref("");
 | 
					 | 
				
			||||||
const error = ref("");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function signup() {
 | 
					const username = ref('');
 | 
				
			||||||
  if (username.value === "" || password.value === "") {
 | 
					const password = ref('');
 | 
				
			||||||
    error.value = "Email and/or Password missing";
 | 
					const password2 = ref('');
 | 
				
			||||||
    return;
 | 
					
 | 
				
			||||||
  }
 | 
					const signup = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
  if (password.value !== password2.value) {
 | 
						if (username.value === '' || password.value === '') {
 | 
				
			||||||
    error.value = "Passwords don't match";
 | 
							message.error('Email and/or Password missing');
 | 
				
			||||||
    return;
 | 
							return;
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
  const res = await Auth.auth_signup(username.value, password.value);
 | 
						if (password.value !== password2.value) {
 | 
				
			||||||
  error.value = isErrorResponse(res)
 | 
							message.error("Passwords don't match");
 | 
				
			||||||
    ? "Signup failed: " + res.message
 | 
							return;
 | 
				
			||||||
    : "Signup successful, please wait till an admin unlocks your account.";
 | 
						}
 | 
				
			||||||
 | 
						const res = await Auth.auth_signup(username.value, password.value);
 | 
				
			||||||
 | 
						if (isErrorResponse(res)) {
 | 
				
			||||||
 | 
							message.error(`Signup failed: ${res.message}`);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							message.success(
 | 
				
			||||||
 | 
								'Signup successful, please wait till an admin unlocks your account.',
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									duration: 10000
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function login() {
 | 
				
			||||||
 | 
						router.replace('login');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onKey(event: KeyboardEvent) {
 | 
				
			||||||
 | 
						if (event.key == 'Enter') signup();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div v-if="error !== ''" v-text="error"></div>
 | 
						<n-card>
 | 
				
			||||||
  <input type="email" placeholder="Email" v-model="username" />
 | 
							<n-grid cols="2" x-gap="16" y-gap="16">
 | 
				
			||||||
  <input type="password" placeholder="Password" v-model="password" />
 | 
								<n-gi span="2">
 | 
				
			||||||
  <input type="password" placeholder="Repeat password" v-model="password2" />
 | 
									<n-input
 | 
				
			||||||
  <button @click="signup()">Signup</button>
 | 
										type="text"
 | 
				
			||||||
  <router-link to="login">Login instead?</router-link>
 | 
										placeholder="Email"
 | 
				
			||||||
 | 
										v-model:value="username"
 | 
				
			||||||
 | 
										autofocus
 | 
				
			||||||
 | 
										:input-props="{ type: 'email' }"
 | 
				
			||||||
 | 
										@keyup="onKey"
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								</n-gi>
 | 
				
			||||||
 | 
								<n-gi span="2">
 | 
				
			||||||
 | 
									<n-input
 | 
				
			||||||
 | 
										type="password"
 | 
				
			||||||
 | 
										placeholder="Password"
 | 
				
			||||||
 | 
										v-model:value="password"
 | 
				
			||||||
 | 
										@keyup="onKey"
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								</n-gi>
 | 
				
			||||||
 | 
								<n-gi span="2">
 | 
				
			||||||
 | 
									<n-input
 | 
				
			||||||
 | 
										type="password"
 | 
				
			||||||
 | 
										placeholder="Repeat password"
 | 
				
			||||||
 | 
										v-model:value="password2"
 | 
				
			||||||
 | 
										@keyup="onKey"
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								</n-gi>
 | 
				
			||||||
 | 
								<n-gi>
 | 
				
			||||||
 | 
									<n-button type="info" @click="signup">Signup</n-button>
 | 
				
			||||||
 | 
								</n-gi>
 | 
				
			||||||
 | 
								<n-gi>
 | 
				
			||||||
 | 
									<n-button ghost @click="login">Login instead?</n-button>
 | 
				
			||||||
 | 
								</n-gi>
 | 
				
			||||||
 | 
							</n-grid>
 | 
				
			||||||
 | 
						</n-card>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped></style>
 | 
					<style scoped></style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,90 +1,138 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { TokenInjectType } from "@/api";
 | 
					import type { TokenInjectType } from '@/api';
 | 
				
			||||||
import { ref, inject } from "vue";
 | 
					import { ref, inject } from 'vue';
 | 
				
			||||||
import { Auth, check_token, isErrorResponse } from "@/api";
 | 
					import { Auth, check_token, isErrorResponse } from '@/api';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						useMessage,
 | 
				
			||||||
 | 
						NInput,
 | 
				
			||||||
 | 
						NGrid,
 | 
				
			||||||
 | 
						NGi,
 | 
				
			||||||
 | 
						NButton,
 | 
				
			||||||
 | 
						NImage,
 | 
				
			||||||
 | 
						NPopover,
 | 
				
			||||||
 | 
						NCard
 | 
				
			||||||
 | 
					} from 'naive-ui';
 | 
				
			||||||
 | 
					import { loadingMsgWrapper } from '@/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const message = useMessage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum state {
 | 
					enum state {
 | 
				
			||||||
  SELECT,
 | 
						SELECT,
 | 
				
			||||||
  MAIL,
 | 
						MAIL,
 | 
				
			||||||
  TOTP,
 | 
						TOTP
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const currentState = ref<state>(state.SELECT);
 | 
					const currentState = ref<state>(state.SELECT);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const error = ref("");
 | 
					const qrImage = ref('');
 | 
				
			||||||
const qrImage = ref("");
 | 
					const secret = ref('');
 | 
				
			||||||
const secret = ref("");
 | 
					const code = ref('');
 | 
				
			||||||
const code = ref("");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
 | 
					const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function selectMail() {
 | 
					const selectMail = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  if (!token) return;
 | 
						if (!token) return;
 | 
				
			||||||
  error.value = "Working...";
 | 
						const res = await Auth.tfa_setup(true, token);
 | 
				
			||||||
  const res = await Auth.tfa_setup(true, token);
 | 
						if (isErrorResponse(res))
 | 
				
			||||||
  if (isErrorResponse(res))
 | 
							message.error(`Failed to select 2fa type: ${res.message}`);
 | 
				
			||||||
    error.value = "Failed to select 2fa type: " + res.message;
 | 
						else currentState.value = state.MAIL;
 | 
				
			||||||
  else {
 | 
					});
 | 
				
			||||||
    error.value = "";
 | 
					 | 
				
			||||||
    currentState.value = state.MAIL;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function selectTotp() {
 | 
					const selectTotp = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  if (!token) return;
 | 
						if (!token) return;
 | 
				
			||||||
  error.value = "Working...";
 | 
						const res = await Auth.tfa_setup(false, token);
 | 
				
			||||||
  const res = await Auth.tfa_setup(false, token);
 | 
						if (isErrorResponse(res))
 | 
				
			||||||
  if (isErrorResponse(res))
 | 
							message.error(`Failed to select 2fa type: ${res.message}`);
 | 
				
			||||||
    error.value = "Failed to select 2fa type: " + res.message;
 | 
						else {
 | 
				
			||||||
  else {
 | 
							qrImage.value = res.qrCode;
 | 
				
			||||||
    qrImage.value = res.qrCode;
 | 
							secret.value = res.secret;
 | 
				
			||||||
    secret.value = res.secret;
 | 
							currentState.value = state.TOTP;
 | 
				
			||||||
    error.value = "";
 | 
						}
 | 
				
			||||||
    currentState.value = state.TOTP;
 | 
					});
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function submit() {
 | 
					const submit = loadingMsgWrapper(message, async () => {
 | 
				
			||||||
  const token = await check_token(jwt);
 | 
						const token = await check_token(jwt);
 | 
				
			||||||
  if (!token) return;
 | 
						if (!token) return;
 | 
				
			||||||
  error.value = "Working...";
 | 
						const res = await Auth.tfa_complete(
 | 
				
			||||||
  const res = await Auth.tfa_complete(
 | 
							currentState.value === state.MAIL,
 | 
				
			||||||
    currentState.value === state.MAIL,
 | 
							code.value,
 | 
				
			||||||
    code.value,
 | 
							token
 | 
				
			||||||
    token
 | 
						);
 | 
				
			||||||
  );
 | 
						if (isErrorResponse(res))
 | 
				
			||||||
  if (isErrorResponse(res))
 | 
							message.error(`Failed to submit code: ${res.message}`);
 | 
				
			||||||
    error.value = "Failed to submit code: " + res.message;
 | 
						else jwt.logout();
 | 
				
			||||||
  else jwt.logout();
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onKey(event: KeyboardEvent) {
 | 
				
			||||||
 | 
						if (event.key == 'Enter') submit();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div v-if="error !== ''" v-text="error"></div>
 | 
						<n-card>
 | 
				
			||||||
  <template v-if="currentState === state.SELECT">
 | 
							<n-grid cols="2" x-gap="16" y-gap="16">
 | 
				
			||||||
    <div>Select 2 Factor authentication type:</div>
 | 
								<template v-if="currentState === state.SELECT">
 | 
				
			||||||
    <div>
 | 
									<n-gi span="2" style="text-align: center">
 | 
				
			||||||
      <button @click="selectMail">Mail</button>
 | 
										Select 2 Factor authentication type
 | 
				
			||||||
      <button @click="selectTotp">Google Authenticator</button>
 | 
									</n-gi>
 | 
				
			||||||
    </div>
 | 
									<n-gi>
 | 
				
			||||||
  </template>
 | 
										<n-button @click="selectMail">Mail</n-button>
 | 
				
			||||||
  <template v-else-if="currentState === state.MAIL">
 | 
									</n-gi>
 | 
				
			||||||
    <div>Please enter the code you got by mail</div>
 | 
									<n-gi style="text-align: right">
 | 
				
			||||||
    <input type="text" placeholder="Code" v-model="code" />
 | 
										<n-button @click="selectTotp"
 | 
				
			||||||
    <button @click="submit()">Submit</button>
 | 
											>Google Authenticator</n-button
 | 
				
			||||||
  </template>
 | 
										>
 | 
				
			||||||
  <template v-else>
 | 
									</n-gi>
 | 
				
			||||||
    <img :src="qrImage" alt="QrCode" />
 | 
								</template>
 | 
				
			||||||
    <details>
 | 
								<template v-else-if="currentState === state.MAIL">
 | 
				
			||||||
      <summary>Show manual input code</summary>
 | 
									<n-gi span="2" style="text-align: center">
 | 
				
			||||||
      {{ secret }}
 | 
										Please enter the code you got by mail
 | 
				
			||||||
    </details>
 | 
									</n-gi>
 | 
				
			||||||
    <div>Please enter the current code</div>
 | 
									<n-gi>
 | 
				
			||||||
    <input type="text" placeholder="Code" v-model="code" />
 | 
										<n-input
 | 
				
			||||||
    <button @click="submit()">Submit</button>
 | 
											type="text"
 | 
				
			||||||
  </template>
 | 
											placeholder="Code"
 | 
				
			||||||
 | 
											maxlength="6"
 | 
				
			||||||
 | 
											v-model:value="code"
 | 
				
			||||||
 | 
											@keyup="onKey"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi style="text-align: right">
 | 
				
			||||||
 | 
										<n-button @click="submit">Submit</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
								</template>
 | 
				
			||||||
 | 
								<template v-else>
 | 
				
			||||||
 | 
									<n-gi span="2" style="text-align: center">
 | 
				
			||||||
 | 
										<n-image :src="qrImage" alt="QrCode" />
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi span="2" style="text-align: center">
 | 
				
			||||||
 | 
										<n-popover placement="bottom" trigger="click">
 | 
				
			||||||
 | 
											<template #trigger>
 | 
				
			||||||
 | 
												<n-button>Show manual input code</n-button>
 | 
				
			||||||
 | 
											</template>
 | 
				
			||||||
 | 
											{{ secret }}
 | 
				
			||||||
 | 
										</n-popover>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi span="2" style="text-align: center">
 | 
				
			||||||
 | 
										Please enter the current code
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi>
 | 
				
			||||||
 | 
										<n-input
 | 
				
			||||||
 | 
											type="text"
 | 
				
			||||||
 | 
											placeholder="Code"
 | 
				
			||||||
 | 
											maxlength="6"
 | 
				
			||||||
 | 
											v-model:value="code"
 | 
				
			||||||
 | 
											@keyup="onKey"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
									<n-gi style="text-align: right">
 | 
				
			||||||
 | 
										<n-button @click="submit">Submit</n-button>
 | 
				
			||||||
 | 
									</n-gi>
 | 
				
			||||||
 | 
								</template>
 | 
				
			||||||
 | 
							</n-grid>
 | 
				
			||||||
 | 
						</n-card>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped></style>
 | 
					<style scoped></style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,18 @@
 | 
				
			|||||||
import { fileURLToPath, URL } from "node:url";
 | 
					import { fileURLToPath, URL } from 'node:url';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { defineConfig } from "vite";
 | 
					import { defineConfig } from 'vite';
 | 
				
			||||||
import vue from "@vitejs/plugin-vue";
 | 
					import vue from '@vitejs/plugin-vue';
 | 
				
			||||||
 | 
					import vueJsx from '@vitejs/plugin-vue-jsx';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// https://vitejs.dev/config/
 | 
					// https://vitejs.dev/config/
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
  plugins: [vue()],
 | 
						plugins: [vue(), vueJsx()],
 | 
				
			||||||
  resolve: {
 | 
						resolve: {
 | 
				
			||||||
    alias: {
 | 
							alias: {
 | 
				
			||||||
      "@": fileURLToPath(new URL("./src", import.meta.url)),
 | 
								'@': fileURLToPath(new URL('./src', import.meta.url))
 | 
				
			||||||
    },
 | 
							}
 | 
				
			||||||
  },
 | 
						},
 | 
				
			||||||
 | 
						build: {
 | 
				
			||||||
 | 
							chunkSizeWarningLimit: 1024 * 1024
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								frontend/vite.dev.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								frontend/vite.dev.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import { fileURLToPath, URL } from 'node:url';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { defineConfig } from 'vite';
 | 
				
			||||||
 | 
					import vue from '@vitejs/plugin-vue';
 | 
				
			||||||
 | 
					import vueJsx from '@vitejs/plugin-vue-jsx';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://vitejs.dev/config/
 | 
				
			||||||
 | 
					export default defineConfig({
 | 
				
			||||||
 | 
						plugins: [vue(), vueJsx()],
 | 
				
			||||||
 | 
						resolve: {
 | 
				
			||||||
 | 
							alias: {
 | 
				
			||||||
 | 
								'@': fileURLToPath(new URL('./src', import.meta.url))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						build: {
 | 
				
			||||||
 | 
							watch: {},
 | 
				
			||||||
 | 
							sourcemap: false,
 | 
				
			||||||
 | 
							minify: false,
 | 
				
			||||||
 | 
							outDir: '../run/static',
 | 
				
			||||||
 | 
							emptyOutDir: true,
 | 
				
			||||||
 | 
							reportCompressedSize: false,
 | 
				
			||||||
 | 
							rollupOptions: {
 | 
				
			||||||
 | 
								output: {
 | 
				
			||||||
 | 
									manualChunks(id) {
 | 
				
			||||||
 | 
										if (id.includes('node_modules')) {
 | 
				
			||||||
 | 
											return 'vendor';
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -2,11 +2,273 @@
 | 
				
			|||||||
# yarn lockfile v1
 | 
					# yarn lockfile v1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@babel/parser@^7.16.4":
 | 
					"@ampproject/remapping@^2.1.0":
 | 
				
			||||||
 | 
					  version "2.2.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
 | 
				
			||||||
 | 
					  integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@jridgewell/gen-mapping" "^0.1.0"
 | 
				
			||||||
 | 
					    "@jridgewell/trace-mapping" "^0.3.9"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/code-frame@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
 | 
				
			||||||
 | 
					  integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/highlight" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/compat-data@^7.18.8":
 | 
				
			||||||
 | 
					  version "7.18.13"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.13.tgz#6aff7b350a1e8c3e40b029e46cbe78e24a913483"
 | 
				
			||||||
 | 
					  integrity sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/core@^7.18.13":
 | 
				
			||||||
 | 
					  version "7.18.13"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.13.tgz#9be8c44512751b05094a4d3ab05fc53a47ce00ac"
 | 
				
			||||||
 | 
					  integrity sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@ampproject/remapping" "^2.1.0"
 | 
				
			||||||
 | 
					    "@babel/code-frame" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/generator" "^7.18.13"
 | 
				
			||||||
 | 
					    "@babel/helper-compilation-targets" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-module-transforms" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helpers" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/parser" "^7.18.13"
 | 
				
			||||||
 | 
					    "@babel/template" "^7.18.10"
 | 
				
			||||||
 | 
					    "@babel/traverse" "^7.18.13"
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.13"
 | 
				
			||||||
 | 
					    convert-source-map "^1.7.0"
 | 
				
			||||||
 | 
					    debug "^4.1.0"
 | 
				
			||||||
 | 
					    gensync "^1.0.0-beta.2"
 | 
				
			||||||
 | 
					    json5 "^2.2.1"
 | 
				
			||||||
 | 
					    semver "^6.3.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/generator@^7.18.13":
 | 
				
			||||||
 | 
					  version "7.18.13"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.13.tgz#59550cbb9ae79b8def15587bdfbaa388c4abf212"
 | 
				
			||||||
 | 
					  integrity sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.13"
 | 
				
			||||||
 | 
					    "@jridgewell/gen-mapping" "^0.3.2"
 | 
				
			||||||
 | 
					    jsesc "^2.5.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-annotate-as-pure@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
 | 
				
			||||||
 | 
					  integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-compilation-targets@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.9"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf"
 | 
				
			||||||
 | 
					  integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/compat-data" "^7.18.8"
 | 
				
			||||||
 | 
					    "@babel/helper-validator-option" "^7.18.6"
 | 
				
			||||||
 | 
					    browserslist "^4.20.2"
 | 
				
			||||||
 | 
					    semver "^6.3.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-create-class-features-plugin@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.13"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.13.tgz#63e771187bd06d234f95fdf8bd5f8b6429de6298"
 | 
				
			||||||
 | 
					  integrity sha512-hDvXp+QYxSRL+23mpAlSGxHMDyIGChm0/AwTfTAAK5Ufe40nCsyNdaYCGuK91phn/fVu9kqayImRDkvNAgdrsA==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-annotate-as-pure" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/helper-environment-visitor" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-function-name" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-member-expression-to-functions" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-optimise-call-expression" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/helper-replace-supers" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-split-export-declaration" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-environment-visitor@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.9"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
 | 
				
			||||||
 | 
					  integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-function-name@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.9"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0"
 | 
				
			||||||
 | 
					  integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/template" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.9"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-hoist-variables@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
 | 
				
			||||||
 | 
					  integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-member-expression-to-functions@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.9"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815"
 | 
				
			||||||
 | 
					  integrity sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.9"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
 | 
				
			||||||
 | 
					  integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-module-transforms@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.9"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712"
 | 
				
			||||||
 | 
					  integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-environment-visitor" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-module-imports" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/helper-simple-access" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/helper-split-export-declaration" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/helper-validator-identifier" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/template" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/traverse" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.9"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-optimise-call-expression@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe"
 | 
				
			||||||
 | 
					  integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.9"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f"
 | 
				
			||||||
 | 
					  integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-replace-supers@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.9"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.9.tgz#1092e002feca980fbbb0bd4d51b74a65c6a500e6"
 | 
				
			||||||
 | 
					  integrity sha512-dNsWibVI4lNT6HiuOIBr1oyxo40HvIVmbwPUm3XZ7wMh4k2WxrxTqZwSqw/eEmXDS9np0ey5M2bz9tBmO9c+YQ==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-environment-visitor" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-member-expression-to-functions" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-optimise-call-expression" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/traverse" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.9"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-simple-access@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea"
 | 
				
			||||||
 | 
					  integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-split-export-declaration@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075"
 | 
				
			||||||
 | 
					  integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-string-parser@^7.18.10":
 | 
				
			||||||
 | 
					  version "7.18.10"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56"
 | 
				
			||||||
 | 
					  integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-validator-identifier@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
 | 
				
			||||||
 | 
					  integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helper-validator-option@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
 | 
				
			||||||
 | 
					  integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helpers@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.9"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9"
 | 
				
			||||||
 | 
					  integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/template" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/traverse" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.9"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/highlight@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
 | 
				
			||||||
 | 
					  integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-validator-identifier" "^7.18.6"
 | 
				
			||||||
 | 
					    chalk "^2.0.0"
 | 
				
			||||||
 | 
					    js-tokens "^4.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/parser@^7.16.4", "@babel/parser@^7.18.10", "@babel/parser@^7.18.13":
 | 
				
			||||||
  version "7.18.13"
 | 
					  version "7.18.13"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4"
 | 
					  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4"
 | 
				
			||||||
  integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==
 | 
					  integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/plugin-syntax-import-meta@^7.10.4":
 | 
				
			||||||
 | 
					  version "7.10.4"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
 | 
				
			||||||
 | 
					  integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-plugin-utils" "^7.10.4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/plugin-syntax-jsx@^7.0.0":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
 | 
				
			||||||
 | 
					  integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-plugin-utils" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/plugin-syntax-typescript@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285"
 | 
				
			||||||
 | 
					  integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-plugin-utils" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/plugin-transform-typescript@^7.18.12":
 | 
				
			||||||
 | 
					  version "7.18.12"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.12.tgz#712e9a71b9e00fde9f8c0238e0cceee86ab2f8fd"
 | 
				
			||||||
 | 
					  integrity sha512-2vjjam0cum0miPkenUbQswKowuxs/NjMwIKEq0zwegRxXk12C9YOF9STXnaUptITOtOJHKHpzvvWYOjbm6tc0w==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-create-class-features-plugin" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-plugin-utils" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/plugin-syntax-typescript" "^7.18.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.18.6":
 | 
				
			||||||
 | 
					  version "7.18.10"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
 | 
				
			||||||
 | 
					  integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/code-frame" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/parser" "^7.18.10"
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.10"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/traverse@^7.0.0", "@babel/traverse@^7.18.13", "@babel/traverse@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.13"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.13.tgz#5ab59ef51a997b3f10c4587d648b9696b6cb1a68"
 | 
				
			||||||
 | 
					  integrity sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/code-frame" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/generator" "^7.18.13"
 | 
				
			||||||
 | 
					    "@babel/helper-environment-visitor" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-function-name" "^7.18.9"
 | 
				
			||||||
 | 
					    "@babel/helper-hoist-variables" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/helper-split-export-declaration" "^7.18.6"
 | 
				
			||||||
 | 
					    "@babel/parser" "^7.18.13"
 | 
				
			||||||
 | 
					    "@babel/types" "^7.18.13"
 | 
				
			||||||
 | 
					    debug "^4.1.0"
 | 
				
			||||||
 | 
					    globals "^11.1.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.13", "@babel/types@^7.18.6", "@babel/types@^7.18.9":
 | 
				
			||||||
 | 
					  version "7.18.13"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a"
 | 
				
			||||||
 | 
					  integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-string-parser" "^7.18.10"
 | 
				
			||||||
 | 
					    "@babel/helper-validator-identifier" "^7.18.6"
 | 
				
			||||||
 | 
					    to-fast-properties "^2.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@css-render/plugin-bem@^0.15.10":
 | 
					"@css-render/plugin-bem@^0.15.10":
 | 
				
			||||||
  version "0.15.11"
 | 
					  version "0.15.11"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@css-render/plugin-bem/-/plugin-bem-0.15.11.tgz#250b853704af1fbb935b8fcd987839dcc9c95ce2"
 | 
					  resolved "https://registry.yarnpkg.com/@css-render/plugin-bem/-/plugin-bem-0.15.11.tgz#250b853704af1fbb935b8fcd987839dcc9c95ce2"
 | 
				
			||||||
@@ -61,6 +323,46 @@
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
 | 
					  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
 | 
				
			||||||
  integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 | 
					  integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@jridgewell/gen-mapping@^0.1.0":
 | 
				
			||||||
 | 
					  version "0.1.1"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
 | 
				
			||||||
 | 
					  integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@jridgewell/set-array" "^1.0.0"
 | 
				
			||||||
 | 
					    "@jridgewell/sourcemap-codec" "^1.4.10"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@jridgewell/gen-mapping@^0.3.2":
 | 
				
			||||||
 | 
					  version "0.3.2"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
 | 
				
			||||||
 | 
					  integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@jridgewell/set-array" "^1.0.1"
 | 
				
			||||||
 | 
					    "@jridgewell/sourcemap-codec" "^1.4.10"
 | 
				
			||||||
 | 
					    "@jridgewell/trace-mapping" "^0.3.9"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@jridgewell/resolve-uri@^3.0.3":
 | 
				
			||||||
 | 
					  version "3.1.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
 | 
				
			||||||
 | 
					  integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
 | 
				
			||||||
 | 
					  version "1.1.2"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
 | 
				
			||||||
 | 
					  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@jridgewell/sourcemap-codec@^1.4.10":
 | 
				
			||||||
 | 
					  version "1.4.14"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
 | 
				
			||||||
 | 
					  integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@jridgewell/trace-mapping@^0.3.9":
 | 
				
			||||||
 | 
					  version "0.3.15"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774"
 | 
				
			||||||
 | 
					  integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@jridgewell/resolve-uri" "^3.0.3"
 | 
				
			||||||
 | 
					    "@jridgewell/sourcemap-codec" "^1.4.10"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@juggle/resize-observer@^3.3.1":
 | 
					"@juggle/resize-observer@^3.3.1":
 | 
				
			||||||
  version "3.4.0"
 | 
					  version "3.4.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
 | 
					  resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
 | 
				
			||||||
@@ -200,6 +502,26 @@
 | 
				
			|||||||
    "@typescript-eslint/types" "5.36.1"
 | 
					    "@typescript-eslint/types" "5.36.1"
 | 
				
			||||||
    eslint-visitor-keys "^3.3.0"
 | 
					    eslint-visitor-keys "^3.3.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@vicons/carbon@^0.12.0":
 | 
				
			||||||
 | 
					  version "0.12.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@vicons/carbon/-/carbon-0.12.0.tgz#dfcc5d6283662eccee55700b2d5c29e688a70f5a"
 | 
				
			||||||
 | 
					  integrity sha512-kCOgr/ZOhZzoiFLJ8pwxMa2TMxrkCUOA22qExPabus35F4+USqzcsxaPoYtqRd9ROOYiHrSqwapak/ywF0D9bg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@vicons/ionicons5@^0.12.0":
 | 
				
			||||||
 | 
					  version "0.12.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@vicons/ionicons5/-/ionicons5-0.12.0.tgz#c39fda04420dfae3b58053faf8aaf3555253299d"
 | 
				
			||||||
 | 
					  integrity sha512-Iy1EUVRpX0WWxeu1VIReR1zsZLMc4fqpt223czR+Rpnrwu7pt46nbnC2ycO7ItI/uqDLJxnbcMC7FujKs9IfFA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@vitejs/plugin-vue-jsx@^2.0.0":
 | 
				
			||||||
 | 
					  version "2.0.1"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-2.0.1.tgz#563a844964f5b025c828b452d6a9882df7194f9a"
 | 
				
			||||||
 | 
					  integrity sha512-lmiR1k9+lrF7LMczO0pxtQ8mOn6XeppJDHxnpxkJQpT5SiKz4SKhKdeNstXaTNuR8qZhUo5X0pJlcocn72Y4Jg==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/core" "^7.18.13"
 | 
				
			||||||
 | 
					    "@babel/plugin-syntax-import-meta" "^7.10.4"
 | 
				
			||||||
 | 
					    "@babel/plugin-transform-typescript" "^7.18.12"
 | 
				
			||||||
 | 
					    "@vue/babel-plugin-jsx" "^1.1.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@vitejs/plugin-vue@^3.0.1":
 | 
					"@vitejs/plugin-vue@^3.0.1":
 | 
				
			||||||
  version "3.0.3"
 | 
					  version "3.0.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.3.tgz#7e3e401ccb30b4380d2279d9849281413f1791ef"
 | 
					  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.3.tgz#7e3e401ccb30b4380d2279d9849281413f1791ef"
 | 
				
			||||||
@@ -255,6 +577,26 @@
 | 
				
			|||||||
    "@volar/typescript-faster" "0.39.5"
 | 
					    "@volar/typescript-faster" "0.39.5"
 | 
				
			||||||
    "@volar/vue-language-core" "0.39.5"
 | 
					    "@volar/vue-language-core" "0.39.5"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@vue/babel-helper-vue-transform-on@^1.0.2":
 | 
				
			||||||
 | 
					  version "1.0.2"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz#9b9c691cd06fc855221a2475c3cc831d774bc7dc"
 | 
				
			||||||
 | 
					  integrity sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@vue/babel-plugin-jsx@^1.1.1":
 | 
				
			||||||
 | 
					  version "1.1.1"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz#0c5bac27880d23f89894cd036a37b55ef61ddfc1"
 | 
				
			||||||
 | 
					  integrity sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-module-imports" "^7.0.0"
 | 
				
			||||||
 | 
					    "@babel/plugin-syntax-jsx" "^7.0.0"
 | 
				
			||||||
 | 
					    "@babel/template" "^7.0.0"
 | 
				
			||||||
 | 
					    "@babel/traverse" "^7.0.0"
 | 
				
			||||||
 | 
					    "@babel/types" "^7.0.0"
 | 
				
			||||||
 | 
					    "@vue/babel-helper-vue-transform-on" "^1.0.2"
 | 
				
			||||||
 | 
					    camelcase "^6.0.0"
 | 
				
			||||||
 | 
					    html-tags "^3.1.0"
 | 
				
			||||||
 | 
					    svg-tags "^1.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@vue/compiler-core@3.2.38", "@vue/compiler-core@^3.2.37":
 | 
					"@vue/compiler-core@3.2.38", "@vue/compiler-core@^3.2.37":
 | 
				
			||||||
  version "3.2.38"
 | 
					  version "3.2.38"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.38.tgz#0a2a7bffd2280ac19a96baf5301838a2cf1964d7"
 | 
					  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.38.tgz#0a2a7bffd2280ac19a96baf5301838a2cf1964d7"
 | 
				
			||||||
@@ -482,6 +824,16 @@ braces@^3.0.2, braces@~3.0.2:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    fill-range "^7.0.1"
 | 
					    fill-range "^7.0.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					browserslist@^4.20.2:
 | 
				
			||||||
 | 
					  version "4.21.3"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
 | 
				
			||||||
 | 
					  integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    caniuse-lite "^1.0.30001370"
 | 
				
			||||||
 | 
					    electron-to-chromium "^1.4.202"
 | 
				
			||||||
 | 
					    node-releases "^2.0.6"
 | 
				
			||||||
 | 
					    update-browserslist-db "^1.0.5"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
call-bind@^1.0.0, call-bind@^1.0.2:
 | 
					call-bind@^1.0.0, call-bind@^1.0.2:
 | 
				
			||||||
  version "1.0.2"
 | 
					  version "1.0.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
 | 
					  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
 | 
				
			||||||
@@ -495,7 +847,17 @@ callsites@^3.0.0:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
 | 
					  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
 | 
				
			||||||
  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 | 
					  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
chalk@^2.4.1:
 | 
					camelcase@^6.0.0:
 | 
				
			||||||
 | 
					  version "6.3.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
 | 
				
			||||||
 | 
					  integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					caniuse-lite@^1.0.30001370:
 | 
				
			||||||
 | 
					  version "1.0.30001388"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001388.tgz#88e01f4591cbd81f9f665f3f078c66b509fbe55d"
 | 
				
			||||||
 | 
					  integrity sha512-znVbq4OUjqgLxMxoNX2ZeeLR0d7lcDiE5uJ4eUiWdml1J1EkxbnQq6opT9jb9SMfJxB0XA16/ziHwni4u1I3GQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					chalk@^2.0.0, chalk@^2.4.1:
 | 
				
			||||||
  version "2.4.2"
 | 
					  version "2.4.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
 | 
					  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
 | 
				
			||||||
  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
 | 
					  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
 | 
				
			||||||
@@ -527,19 +889,6 @@ chalk@^4.0.0:
 | 
				
			|||||||
  optionalDependencies:
 | 
					  optionalDependencies:
 | 
				
			||||||
    fsevents "~2.3.2"
 | 
					    fsevents "~2.3.2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class-transformer@^0.5.1:
 | 
					 | 
				
			||||||
  version "0.5.1"
 | 
					 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336"
 | 
					 | 
				
			||||||
  integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class-validator@^0.13.2:
 | 
					 | 
				
			||||||
  version "0.13.2"
 | 
					 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.13.2.tgz#64b031e9f3f81a1e1dcd04a5d604734608b24143"
 | 
					 | 
				
			||||||
  integrity sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==
 | 
					 | 
				
			||||||
  dependencies:
 | 
					 | 
				
			||||||
    libphonenumber-js "^1.9.43"
 | 
					 | 
				
			||||||
    validator "^13.7.0"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
color-convert@^1.9.0:
 | 
					color-convert@^1.9.0:
 | 
				
			||||||
  version "1.9.3"
 | 
					  version "1.9.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
 | 
					  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
 | 
				
			||||||
@@ -576,6 +925,13 @@ concat-map@0.0.1:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 | 
					  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 | 
				
			||||||
  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
 | 
					  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					convert-source-map@^1.7.0:
 | 
				
			||||||
 | 
					  version "1.8.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
 | 
				
			||||||
 | 
					  integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    safe-buffer "~5.1.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
cross-spawn@^6.0.5:
 | 
					cross-spawn@^6.0.5:
 | 
				
			||||||
  version "6.0.5"
 | 
					  version "6.0.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
 | 
					  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
 | 
				
			||||||
@@ -621,16 +977,16 @@ csstype@~3.0.5:
 | 
				
			|||||||
  integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
 | 
					  integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
date-fns-tz@^1.3.3:
 | 
					date-fns-tz@^1.3.3:
 | 
				
			||||||
  version "1.3.6"
 | 
					  version "1.3.7"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.6.tgz#4195a58a2f86eda55ea69fb477f3ed8a6e2188ac"
 | 
					  resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.7.tgz#e8e9d2aaceba5f1cc0e677631563081fdcb0e69a"
 | 
				
			||||||
  integrity sha512-C8q7mErvG4INw1ZwAFmPlGjEo5Sv4udjKVbTc03zpP9cu6cp5AemFzKhz0V68LGcWEtX5mJudzzg3G04emIxLA==
 | 
					  integrity sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
date-fns@^2.28.0:
 | 
					date-fns@^2.28.0:
 | 
				
			||||||
  version "2.29.2"
 | 
					  version "2.29.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931"
 | 
					  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931"
 | 
				
			||||||
  integrity sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA==
 | 
					  integrity sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
 | 
					debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
 | 
				
			||||||
  version "4.3.4"
 | 
					  version "4.3.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
 | 
					  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
 | 
				
			||||||
  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
 | 
					  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
 | 
				
			||||||
@@ -669,6 +1025,11 @@ doctrine@^3.0.0:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    esutils "^2.0.2"
 | 
					    esutils "^2.0.2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					electron-to-chromium@^1.4.202:
 | 
				
			||||||
 | 
					  version "1.4.240"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.240.tgz#b11fb838f2e79f34fbe8b57eec55e7e5d81ee6ea"
 | 
				
			||||||
 | 
					  integrity sha512-r20dUOtZ4vUPTqAajDGonIM1uas5tf85Up+wPdtNBNvBSqGCfkpvMVvQ1T8YJzPV9/Y9g3FbUDcXb94Rafycow==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
error-ex@^1.3.1:
 | 
					error-ex@^1.3.1:
 | 
				
			||||||
  version "1.3.2"
 | 
					  version "1.3.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
 | 
					  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
 | 
				
			||||||
@@ -841,6 +1202,11 @@ esbuild@^0.14.47:
 | 
				
			|||||||
    esbuild-windows-64 "0.14.54"
 | 
					    esbuild-windows-64 "0.14.54"
 | 
				
			||||||
    esbuild-windows-arm64 "0.14.54"
 | 
					    esbuild-windows-arm64 "0.14.54"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					escalade@^3.1.1:
 | 
				
			||||||
 | 
					  version "3.1.1"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
 | 
				
			||||||
 | 
					  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
escape-string-regexp@^1.0.5:
 | 
					escape-string-regexp@^1.0.5:
 | 
				
			||||||
  version "1.0.5"
 | 
					  version "1.0.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 | 
					  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 | 
				
			||||||
@@ -1136,6 +1502,11 @@ functions-have-names@^1.2.2:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
 | 
					  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
 | 
				
			||||||
  integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
 | 
					  integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gensync@^1.0.0-beta.2:
 | 
				
			||||||
 | 
					  version "1.0.0-beta.2"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
 | 
				
			||||||
 | 
					  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
 | 
					get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
 | 
				
			||||||
  version "1.1.2"
 | 
					  version "1.1.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598"
 | 
					  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598"
 | 
				
			||||||
@@ -1179,6 +1550,11 @@ glob@^7.1.3:
 | 
				
			|||||||
    once "^1.3.0"
 | 
					    once "^1.3.0"
 | 
				
			||||||
    path-is-absolute "^1.0.0"
 | 
					    path-is-absolute "^1.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					globals@^11.1.0:
 | 
				
			||||||
 | 
					  version "11.12.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
 | 
				
			||||||
 | 
					  integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
globals@^13.15.0:
 | 
					globals@^13.15.0:
 | 
				
			||||||
  version "13.17.0"
 | 
					  version "13.17.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4"
 | 
					  resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4"
 | 
				
			||||||
@@ -1259,6 +1635,11 @@ hosted-git-info@^2.1.4:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
 | 
					  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
 | 
				
			||||||
  integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
 | 
					  integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html-tags@^3.1.0:
 | 
				
			||||||
 | 
					  version "3.2.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961"
 | 
				
			||||||
 | 
					  integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ignore@^5.2.0:
 | 
					ignore@^5.2.0:
 | 
				
			||||||
  version "5.2.0"
 | 
					  version "5.2.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
 | 
					  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
 | 
				
			||||||
@@ -1446,6 +1827,11 @@ isexe@^2.0.0:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
 | 
					  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
 | 
				
			||||||
  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
 | 
					  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					js-tokens@^4.0.0:
 | 
				
			||||||
 | 
					  version "4.0.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
 | 
				
			||||||
 | 
					  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
js-yaml@^4.1.0:
 | 
					js-yaml@^4.1.0:
 | 
				
			||||||
  version "4.1.0"
 | 
					  version "4.1.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
 | 
					  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
 | 
				
			||||||
@@ -1453,6 +1839,11 @@ js-yaml@^4.1.0:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    argparse "^2.0.1"
 | 
					    argparse "^2.0.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jsesc@^2.5.1:
 | 
				
			||||||
 | 
					  version "2.5.2"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
 | 
				
			||||||
 | 
					  integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
json-parse-better-errors@^1.0.1:
 | 
					json-parse-better-errors@^1.0.1:
 | 
				
			||||||
  version "1.0.2"
 | 
					  version "1.0.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
 | 
					  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
 | 
				
			||||||
@@ -1468,6 +1859,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
 | 
					  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
 | 
				
			||||||
  integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
 | 
					  integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					json5@^2.2.1:
 | 
				
			||||||
 | 
					  version "2.2.1"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
 | 
				
			||||||
 | 
					  integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jwt-decode@^3.1.2:
 | 
					jwt-decode@^3.1.2:
 | 
				
			||||||
  version "3.1.2"
 | 
					  version "3.1.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
 | 
					  resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
 | 
				
			||||||
@@ -1481,11 +1877,6 @@ levn@^0.4.1:
 | 
				
			|||||||
    prelude-ls "^1.2.1"
 | 
					    prelude-ls "^1.2.1"
 | 
				
			||||||
    type-check "~0.4.0"
 | 
					    type-check "~0.4.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
libphonenumber-js@^1.9.43:
 | 
					 | 
				
			||||||
  version "1.10.13"
 | 
					 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.13.tgz#0b5833c7fdbf671140530d83531c6753f7e0ea3c"
 | 
					 | 
				
			||||||
  integrity sha512-b74iyWmwb4GprAUPjPkJ11GTC7KX4Pd3onpJfKxYyY8y9Rbb4ERY47LvCMEDM09WD3thiLDMXtkfDK/AX+zT7Q==
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
load-json-file@^4.0.0:
 | 
					load-json-file@^4.0.0:
 | 
				
			||||||
  version "4.0.0"
 | 
					  version "4.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
 | 
					  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
 | 
				
			||||||
@@ -1575,9 +1966,9 @@ ms@2.1.2:
 | 
				
			|||||||
  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 | 
					  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
naive-ui@^2.32.1:
 | 
					naive-ui@^2.32.1:
 | 
				
			||||||
  version "2.33.1"
 | 
					  version "2.33.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/naive-ui/-/naive-ui-2.33.1.tgz#ef1046b727145e868c4be32686fd6073219f07ac"
 | 
					  resolved "https://registry.yarnpkg.com/naive-ui/-/naive-ui-2.33.2.tgz#c74e8b7c944f6af18cd850bd640f6d3485a47f05"
 | 
				
			||||||
  integrity sha512-S8iS5TsnJ5PAbUCCC+IGjW7H6fYJF5s0HTzuUjqRLS8C1tFxmWhKkBZU1db/vg/4O5GKEyjaoq4ZSzRHOwRTcQ==
 | 
					  integrity sha512-XT18dOE7dK15xedO9MlrPsD3AXBKncr0lqlsxakHl/DckqOaAbdA7yxDl/qtVTBC+1Rlf29cFP/th7P7DSy5zg==
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    "@css-render/plugin-bem" "^0.15.10"
 | 
					    "@css-render/plugin-bem" "^0.15.10"
 | 
				
			||||||
    "@css-render/vue3-ssr" "^0.15.10"
 | 
					    "@css-render/vue3-ssr" "^0.15.10"
 | 
				
			||||||
@@ -1612,6 +2003,11 @@ nice-try@^1.0.4:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
 | 
					  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
 | 
				
			||||||
  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 | 
					  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					node-releases@^2.0.6:
 | 
				
			||||||
 | 
					  version "2.0.6"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
 | 
				
			||||||
 | 
					  integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
normalize-package-data@^2.3.2:
 | 
					normalize-package-data@^2.3.2:
 | 
				
			||||||
  version "2.5.0"
 | 
					  version "2.5.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
 | 
					  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
 | 
				
			||||||
@@ -1902,10 +2298,15 @@ safe-buffer@^5.1.2, safe-buffer@~5.2.0:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
 | 
					  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
 | 
				
			||||||
  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 | 
					  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					safe-buffer@~5.1.1:
 | 
				
			||||||
 | 
					  version "5.1.2"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
 | 
				
			||||||
 | 
					  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sass@^1.32.7:
 | 
					sass@^1.32.7:
 | 
				
			||||||
  version "1.54.6"
 | 
					  version "1.54.8"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.6.tgz#5a12c268db26555c335028e355d6b7b1a5b9b4c8"
 | 
					  resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.8.tgz#4adef0dd86ea2b1e4074f551eeda4fc5f812a996"
 | 
				
			||||||
  integrity sha512-DUqJjR2WxXBcZjRSZX5gCVyU+9fuC2qDfFzoKX9rV4rCOcec5mPtEafTcfsyL3YJuLONjWylBne+uXVh5rrmFw==
 | 
					  integrity sha512-ib4JhLRRgbg6QVy6bsv5uJxnJMTS2soVcCp9Y88Extyy13A8vV0G1fAwujOzmNkFQbR3LvedudAMbtuNRPbQww==
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    chokidar ">=3.0.0 <4.0.0"
 | 
					    chokidar ">=3.0.0 <4.0.0"
 | 
				
			||||||
    immutable "^4.0.0"
 | 
					    immutable "^4.0.0"
 | 
				
			||||||
@@ -1921,6 +2322,11 @@ seemly@^0.3.6:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
 | 
					  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
 | 
				
			||||||
  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 | 
					  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					semver@^6.3.0:
 | 
				
			||||||
 | 
					  version "6.3.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
 | 
				
			||||||
 | 
					  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
semver@^7.3.5, semver@^7.3.6, semver@^7.3.7:
 | 
					semver@^7.3.5, semver@^7.3.6, semver@^7.3.7:
 | 
				
			||||||
  version "7.3.7"
 | 
					  version "7.3.7"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
 | 
					  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
 | 
				
			||||||
@@ -2090,11 +2496,21 @@ supports-preserve-symlinks-flag@^1.0.0:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
 | 
					  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
 | 
				
			||||||
  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 | 
					  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					svg-tags@^1.0.0:
 | 
				
			||||||
 | 
					  version "1.0.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
 | 
				
			||||||
 | 
					  integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
text-table@^0.2.0:
 | 
					text-table@^0.2.0:
 | 
				
			||||||
  version "0.2.0"
 | 
					  version "0.2.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
 | 
					  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
 | 
				
			||||||
  integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
 | 
					  integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					to-fast-properties@^2.0.0:
 | 
				
			||||||
 | 
					  version "2.0.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
 | 
				
			||||||
 | 
					  integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
to-regex-range@^5.0.1:
 | 
					to-regex-range@^5.0.1:
 | 
				
			||||||
  version "5.0.1"
 | 
					  version "5.0.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
 | 
					  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
 | 
				
			||||||
@@ -2146,6 +2562,14 @@ unbox-primitive@^1.0.2:
 | 
				
			|||||||
    has-symbols "^1.0.3"
 | 
					    has-symbols "^1.0.3"
 | 
				
			||||||
    which-boxed-primitive "^1.0.2"
 | 
					    which-boxed-primitive "^1.0.2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					update-browserslist-db@^1.0.5:
 | 
				
			||||||
 | 
					  version "1.0.7"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.7.tgz#16279639cff1d0f800b14792de43d97df2d11b7d"
 | 
				
			||||||
 | 
					  integrity sha512-iN/XYesmZ2RmmWAiI4Z5rq0YqSiv0brj9Ce9CfhNE4xIW2h+MFxcgkxIzZ+ShkFPUkjU3gQ+3oypadD3RAMtrg==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    escalade "^3.1.1"
 | 
				
			||||||
 | 
					    picocolors "^1.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
uri-js@^4.2.2:
 | 
					uri-js@^4.2.2:
 | 
				
			||||||
  version "4.4.1"
 | 
					  version "4.4.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
 | 
					  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
 | 
				
			||||||
@@ -2183,11 +2607,6 @@ validate-npm-package-license@^3.0.1:
 | 
				
			|||||||
    spdx-correct "^3.0.0"
 | 
					    spdx-correct "^3.0.0"
 | 
				
			||||||
    spdx-expression-parse "^3.0.0"
 | 
					    spdx-expression-parse "^3.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
validator@^13.7.0:
 | 
					 | 
				
			||||||
  version "13.7.0"
 | 
					 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
 | 
					 | 
				
			||||||
  integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
vdirs@^0.1.4, vdirs@^0.1.8:
 | 
					vdirs@^0.1.4, vdirs@^0.1.8:
 | 
				
			||||||
  version "0.1.8"
 | 
					  version "0.1.8"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/vdirs/-/vdirs-0.1.8.tgz#a103bc43baca738f8dea912a7e9737154a19dbc2"
 | 
					  resolved "https://registry.yarnpkg.com/vdirs/-/vdirs-0.1.8.tgz#a103bc43baca738f8dea912a7e9737154a19dbc2"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user