Rewrote Frontend
This commit is contained in:
parent
0939525cf3
commit
16876e090d
@ -26,7 +26,6 @@ build_backend:
|
|||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- server
|
- server
|
||||||
expire_in: 1h
|
|
||||||
|
|
||||||
test_and_build_frontend:
|
test_and_build_frontend:
|
||||||
image: node:latest
|
image: node:latest
|
||||||
@ -44,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
Normal file
9
backend/.idea/cmake.xml
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>
|
@ -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(mailio 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
|
||||||
mailio
|
|
||||||
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_link_libraries(backend iphlpapi)
|
||||||
target_compile_definitions(backend PRIVATE NOMINMAX _WIN32_WINNT=0x0A00)
|
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,13 +55,14 @@ 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&) {
|
||||||
|
@ -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,21 +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);
|
|
||||||
|
|
||||||
return "data:image/png;base64," + Botan::base64_encode(secret);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace api {
|
namespace api {
|
||||||
@ -64,7 +57,7 @@ namespace api {
|
|||||||
std::string code = create_totp_qrcode(user, b32_secret);
|
std::string code = create_totp_qrcode(user, b32_secret);
|
||||||
cbk(dto::Responses::get_tfa_setup_res(b32_secret, code));
|
cbk(dto::Responses::get_tfa_setup_res(b32_secret, code));
|
||||||
}
|
}
|
||||||
} catch (const std::exception&) {
|
} catch (const std::exception& e) {
|
||||||
cbk(dto::Responses::get_badreq_res("Validation error"));
|
cbk(dto::Responses::get_badreq_res("Validation error"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
#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 <mailio/smtp.hpp>
|
#include <SMTPMail.h>
|
||||||
|
|
||||||
#include "controllers/controllers.h"
|
#include "controllers/controllers.h"
|
||||||
#include "db/db.h"
|
#include "db/db.h"
|
||||||
@ -43,15 +43,17 @@ namespace api {
|
|||||||
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));
|
||||||
|
|
||||||
mailio::message msg;
|
drogon::app().getPlugin<SMTPMail>()->sendEmail(
|
||||||
msg.from(mailio::mail_address("Fileserver", "fileserver@mattv.de"));
|
"mail.mattv.de",
|
||||||
msg.add_recipient(mailio::mail_address(user.getValueOfName(), user.getValueOfName()));
|
587,
|
||||||
msg.subject("Subject: Fileserver - Email 2fa code");
|
"fileserver@mattv.de",
|
||||||
msg.content("Your code is: " + std::string(totp) +"\r\nIt is valid for 5 Minutes");
|
user.getValueOfName(),
|
||||||
|
"MFileserver - Email 2fa code",
|
||||||
mailio::smtps conn("mail.mattv.de", 587);
|
"Your code is: " + std::string(totp) +"\r\nIt is valid for 5 Minutes",
|
||||||
conn.authenticate("no-reply@mattv.de", "noreplyLONGPASS123", mailio::smtps::auth_method_t::START_TLS);
|
"no-reply@mattv.de",
|
||||||
conn.submit(msg);
|
"noreplyLONGPASS123",
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,32 @@ 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::download_base64, "/download_base64/{}", 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 +120,12 @@ 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 download_base64(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)
|
||||||
db::MapperInode inode_mapper(drogon::app().getDbClient());
|
std::filesystem::remove(p.string() + "_preview.png");
|
||||||
inode_mapper.deleteOne(node);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@ -199,6 +490,7 @@ namespace api {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inode->getValueOfIsFile() != 0) {
|
||||||
std::filesystem::path p("./files");
|
std::filesystem::path p("./files");
|
||||||
p /= std::to_string(inode->getValueOfId());
|
p /= std::to_string(inode->getValueOfId());
|
||||||
|
|
||||||
@ -206,6 +498,100 @@ namespace api {
|
|||||||
p.string(),
|
p.string(),
|
||||||
inode->getValueOfName()
|
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");
|
||||||
|
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::download_base64(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());
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::string mime = mime_type_map.at(name.extension().string());
|
||||||
|
std::ifstream file(p, std::ios::in | std::ios::binary);
|
||||||
|
std::vector<uint8_t> content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||||
|
|
||||||
|
cbk(dto::Responses::get_download_base64_res("data:" + mime + ";base64," + Botan::base64_encode(content)));
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
cbk(dto::Responses::get_badreq_res("Invalid file type"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
try {
|
||||||
|
cbk(dto::Responses::get_type_res(mime_type_map.at(name.extension().string())));
|
||||||
|
} 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";
|
||||||
|
|
||||||
|
@ -30,6 +30,16 @@ namespace dto {
|
|||||||
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");
|
||||||
|
@ -13,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,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([]{
|
||||||
@ -76,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;
|
||||||
@ -101,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())
|
||||||
@ -123,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",
|
||||||
"mailio",
|
|
||||||
"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>
|
||||||
</nav>
|
<template #extra>
|
||||||
|
<n-menu
|
||||||
|
v-if="jwt != null"
|
||||||
|
mode="horizontal"
|
||||||
|
:options="menuOptions"
|
||||||
|
@update:value="handleUpdateValue"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</n-page-header>
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
padding: 2em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
.nav-icon {
|
||||||
padding: 30px;
|
top: 0.25em;
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: bold;
|
|
||||||
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">
|
||||||
|
<n-dialog-provider>
|
||||||
|
<App />
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-message-provider>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div>Loading...</div>
|
<div><n-spin size="small" />Loading...</div>
|
||||||
</template>
|
</template>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
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
|
||||||
);
|
);
|
||||||
@ -20,11 +21,11 @@ export const set_role = (
|
|||||||
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
|
||||||
);
|
);
|
||||||
@ -32,11 +33,11 @@ export const logout = (
|
|||||||
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
|
||||||
);
|
);
|
||||||
@ -44,11 +45,11 @@ export const delete_user = (
|
|||||||
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,73 +1,66 @@
|
|||||||
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
|
|
||||||
| Responses.ErrorResponse
|
|
||||||
> =>
|
|
||||||
post<Requests.Auth.LoginRequest>("/api/auth/login", {
|
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
otp: otp,
|
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
|
|
||||||
> {
|
|
||||||
return post_token<Requests.Auth.TfaSetup>(
|
|
||||||
"/api/auth/2fa/setup",
|
|
||||||
{
|
{
|
||||||
mail,
|
mail
|
||||||
},
|
},
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
@ -77,17 +70,17 @@ 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,16 +1,17 @@
|
|||||||
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
|
||||||
@ -18,9 +19,9 @@ export const post_token = <T extends Requests.BaseRequest>(
|
|||||||
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);
|
||||||
@ -34,10 +35,10 @@ export const post_token_form = (
|
|||||||
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);
|
||||||
@ -52,11 +53,10 @@ export const get = (url: string) =>
|
|||||||
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,39 +1,40 @@
|
|||||||
|
import type { Requests, Responses, UploadFile } from '@/dto';
|
||||||
import {
|
import {
|
||||||
Responses,
|
|
||||||
Requests,
|
|
||||||
get_token,
|
get_token,
|
||||||
post_token,
|
post_token,
|
||||||
post_token_form,
|
post_token_form,
|
||||||
isErrorResponse,
|
isErrorResponse
|
||||||
} from "./base";
|
} 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>(
|
||||||
|
'/api/fs/createFolder',
|
||||||
{
|
{
|
||||||
parent: parent,
|
parent: parent,
|
||||||
name: name,
|
name: name
|
||||||
},
|
},
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
@ -42,43 +43,88 @@ 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>(
|
||||||
|
'/api/fs/createFile',
|
||||||
{
|
{
|
||||||
parent: parent,
|
parent: parent,
|
||||||
name: name,
|
name: name
|
||||||
},
|
},
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
|
|
||||||
export const delete_node = (
|
export const create_zip = (
|
||||||
|
token: string,
|
||||||
|
nodes: number[]
|
||||||
|
): Promise<Responses.CreateZip | Responses.Error> =>
|
||||||
|
post_token<Requests.CreateZip>(
|
||||||
|
'/api/fs/create_zip',
|
||||||
|
{
|
||||||
|
nodes: nodes
|
||||||
|
},
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
export const download_preview = (
|
||||||
token: string,
|
token: string,
|
||||||
node: number
|
node: number
|
||||||
): Promise<Responses.FS.DeleteResponse | Responses.ErrorResponse> =>
|
): Promise<Responses.DownloadBase64 | Responses.Error> =>
|
||||||
post_token(`/api/fs/delete/${node}`, {}, token);
|
get_token(`/api/fs/download_preview/${node}`, token);
|
||||||
|
|
||||||
export const upload_file = async (
|
export const download_base64 = (
|
||||||
token: string,
|
token: string,
|
||||||
parent: number,
|
node: number
|
||||||
file: File,
|
): Promise<Responses.DownloadBase64 | Responses.Error> =>
|
||||||
|
get_token(`/api/fs/download_base64/${node}`, token);
|
||||||
|
|
||||||
|
export const get_type = (
|
||||||
|
token: string,
|
||||||
|
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
|
onProgress: (progressEvent: ProgressEvent) => void
|
||||||
): Promise<Responses.FS.UploadFileResponse | Responses.ErrorResponse> => {
|
): Promise<Responses.Success | Responses.Error> {
|
||||||
const node = await create_file(token, parent, file.name);
|
const node = await create_file(token, file.parent, file.file.name);
|
||||||
if (isErrorResponse(node)) return node;
|
if (isErrorResponse(node)) return node;
|
||||||
|
if ('exists' in node && !node.isFile)
|
||||||
|
return { statusCode: 400, message: 'File exists as folder' };
|
||||||
|
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.set("file", file);
|
form.set('file', file.file);
|
||||||
return post_token_form(`/api/fs/upload/${node.id}`, form, token, onProgress);
|
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,8 +1,8 @@
|
|||||||
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
|
||||||
|
@ -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>
|
|
45
frontend/src/components/FileViewer/AudioVideoDownload.tsx
Normal file
45
frontend/src/components/FileViewer/AudioVideoDownload.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import { NProgress } from 'naive-ui';
|
||||||
|
import filesize from 'filesize';
|
||||||
|
import { Music, Video } from '@vicons/carbon';
|
||||||
|
import type { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider';
|
||||||
|
|
||||||
|
export default function createAudioVideoDialog(
|
||||||
|
dialog: DialogApiInjection,
|
||||||
|
video: boolean
|
||||||
|
) {
|
||||||
|
const progress = ref(0);
|
||||||
|
const total = ref(1);
|
||||||
|
const percentage = ref(0);
|
||||||
|
const dia = dialog.create({
|
||||||
|
title: video ? 'Loading video...' : 'Loading audio...',
|
||||||
|
closable: false,
|
||||||
|
closeOnEsc: false,
|
||||||
|
maskClosable: false,
|
||||||
|
icon: () => (video ? <Video /> : <Music />),
|
||||||
|
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 };
|
||||||
|
}
|
129
frontend/src/components/FileViewer/FileViewer.vue
Normal file
129
frontend/src/components/FileViewer/FileViewer.vue
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<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 createAudioVideoDialog from '@/components/FileViewer/AudioVideoDownload';
|
||||||
|
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 loadAudioOrVideo() {
|
||||||
|
const token = await check_token(jwt);
|
||||||
|
if (!token) return;
|
||||||
|
const { progress, total, percentage, dia } = createAudioVideoDialog(
|
||||||
|
dialog,
|
||||||
|
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')) {
|
||||||
|
const dataResp = await FS.download_base64(token, node.id);
|
||||||
|
if (isErrorResponse(dataResp)) return;
|
||||||
|
src.value = dataResp.data;
|
||||||
|
fileType.value = fileTypes.IMAGE;
|
||||||
|
}
|
||||||
|
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" />
|
||||||
|
<n-image
|
||||||
|
v-else-if="fileType === fileTypes.IMAGE"
|
||||||
|
:src="src"
|
||||||
|
:alt="node.name"
|
||||||
|
/>
|
||||||
|
<template
|
||||||
|
v-else-if="
|
||||||
|
fileType === fileTypes.VIDEO || fileType === fileTypes.AUDIO
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
v-if="fileType === fileTypes.VIDEO && src !== ''"
|
||||||
|
:src="src"
|
||||||
|
controls
|
||||||
|
/>
|
||||||
|
<audio
|
||||||
|
v-else-if="fileType === fileTypes.AUDIO && src !== ''"
|
||||||
|
:src="src"
|
||||||
|
controls
|
||||||
|
/>
|
||||||
|
<n-button v-else @click="loadAudioOrVideo">
|
||||||
|
<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,43 +1,66 @@
|
|||||||
<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;
|
percentage.value = 100;
|
||||||
if (isErrorResponse(resp)) {
|
if (isErrorResponse(resp)) {
|
||||||
err.value = resp.message ?? "Error";
|
err.value = resp.message ?? 'Error';
|
||||||
status.value = "error";
|
status.value = 'error';
|
||||||
} else status.value = "success";
|
} else {
|
||||||
|
status.value = 'success';
|
||||||
|
shown.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
startUpload,
|
startUpload
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Transition name="slide-up">
|
||||||
|
<div class="container" v-show="shown">
|
||||||
<div v-if="percentage < 100">
|
<div v-if="percentage < 100">
|
||||||
{{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} -
|
{{ file.fullName }} -
|
||||||
{{ Math.floor(percentage * 1000) / 1000 }}%
|
{{
|
||||||
|
filesize(progress, {
|
||||||
|
base: 2,
|
||||||
|
standard: 'jedec'
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/
|
||||||
|
{{
|
||||||
|
filesize(file.file.size, {
|
||||||
|
base: 2,
|
||||||
|
standard: 'jedec'
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
- {{ Math.floor(percentage * 1000) / 1000 }}%
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div>
|
<div v-else-if="err !== ''">
|
||||||
<div v-else>{{ file.name }} - Completed</div>
|
{{ file.fullName }} - Error: {{ err }}
|
||||||
|
</div>
|
||||||
|
<div v-else>{{ file.fullName }} - Completed</div>
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="percentage"
|
:percentage="percentage"
|
||||||
@ -47,6 +70,24 @@ defineExpose({
|
|||||||
fill-border-radius="10px 0"
|
fill-border-radius="10px 0"
|
||||||
:show-indicator="false"
|
: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: InputEvent) {
|
||||||
|
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,43 +1,43 @@
|
|||||||
<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) {
|
||||||
|
await new Promise<void>((resolve) =>
|
||||||
|
allProms.push(entry.startUpload(token, resolve))
|
||||||
);
|
);
|
||||||
done.value = true;
|
}
|
||||||
await canClose;
|
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>
|
||||||
|
|
||||||
|
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",
|
path: '/signup',
|
||||||
name: "login",
|
name: 'signup',
|
||||||
component: LoginView,
|
component: SignupView
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/signup",
|
path: '/fs/:node_id',
|
||||||
name: "signup",
|
name: 'fs',
|
||||||
component: SignupView,
|
component: FSView
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/fs/:node_id",
|
path: '/set_token',
|
||||||
name: "fs",
|
component: SetTokenView
|
||||||
component: FSView,
|
}
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
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,13 +1,24 @@
|
|||||||
<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();
|
||||||
@ -15,90 +26,127 @@ onBeforeRouteUpdate(async () => {
|
|||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
await updatePanel();
|
await updatePanel();
|
||||||
});
|
});
|
||||||
async function updatePanel() {
|
|
||||||
|
const updatePanel = loadingMsgWrapper(message, async () => {
|
||||||
const token = await check_token(jwt);
|
const token = await check_token(jwt);
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
const res = await Admin.get_users(token);
|
const res = await Admin.get_users(token);
|
||||||
if (isErrorResponse(res)) return router.replace({ path: "/" });
|
if (isErrorResponse(res)) return router.replace({ path: '/' });
|
||||||
users.value = res.users;
|
users.value = res.users;
|
||||||
}
|
});
|
||||||
|
|
||||||
async function setRole(user: number, roleStr: string) {
|
const setRole = loadingMsgWrapper(
|
||||||
|
message,
|
||||||
|
async (user: number, role: number) => {
|
||||||
const token = await check_token(jwt);
|
const token = await check_token(jwt);
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
const res = await Admin.set_role(user, parseInt(roleStr, 10), token);
|
const res = await Admin.set_role(user, role, token);
|
||||||
if (isErrorResponse(res)) console.error(res.message);
|
if (isErrorResponse(res)) console.error(res.message);
|
||||||
await updatePanel();
|
await updatePanel();
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
async function disableTfa(user: number) {
|
const action = (
|
||||||
|
func: (
|
||||||
|
user: number,
|
||||||
|
token: string
|
||||||
|
) => Promise<Responses.Success | Responses.Error>
|
||||||
|
) => {
|
||||||
|
return loadingMsgWrapper(message, async (user: number) => {
|
||||||
const token = await check_token(jwt);
|
const token = await check_token(jwt);
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
const res = await Admin.disable_tfa(user, token);
|
const res = await func(user, token);
|
||||||
if (isErrorResponse(res)) console.error(res.message);
|
if (isErrorResponse(res)) console.error(res.message);
|
||||||
await updatePanel();
|
await updatePanel();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoutUser = action(Admin.logout);
|
||||||
|
const disableTfa = action(Admin.disable_tfa);
|
||||||
|
const deleteUser = action(Admin.delete_user);
|
||||||
|
|
||||||
|
const selectOptions: SelectOption[] = [
|
||||||
|
{
|
||||||
|
label: 'Disabled',
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'User',
|
||||||
|
value: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Admin',
|
||||||
|
value: 2
|
||||||
}
|
}
|
||||||
|
];
|
||||||
|
|
||||||
async function logoutUser(user: number) {
|
const columns: DataTableColumn<Responses.GetUsersEntry>[] = [
|
||||||
const token = await check_token(jwt);
|
{
|
||||||
if (!token) return;
|
title: 'Name',
|
||||||
|
key: 'name'
|
||||||
const res = await Admin.logout(user, token);
|
},
|
||||||
if (isErrorResponse(res)) console.error(res.message);
|
{
|
||||||
await updatePanel();
|
title: 'Type',
|
||||||
|
key: 'gitlab',
|
||||||
|
render(user) {
|
||||||
|
return user.gitlab ? 'Gitlab' : 'Password';
|
||||||
}
|
}
|
||||||
|
},
|
||||||
async function deleteUser(user: number) {
|
{
|
||||||
const token = await check_token(jwt);
|
title: 'Role',
|
||||||
if (!token) return;
|
key: 'role',
|
||||||
|
minWidth: 120,
|
||||||
const res = await Admin.delete_user(user, token);
|
render(user) {
|
||||||
if (isErrorResponse(res)) console.error(res.message);
|
return (
|
||||||
await updatePanel();
|
<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,28 +1,46 @@
|
|||||||
<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) => {
|
||||||
@ -43,23 +61,32 @@ async function gotoRoot() {
|
|||||||
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}`">
|
||||||
|
{{ seg.path }}
|
||||||
|
</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" />
|
<FileViewer v-else :node="node" />
|
||||||
</div>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<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);
|
||||||
@ -15,8 +15,8 @@ async function start_redirect() {
|
|||||||
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 }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await (requestOtp.value
|
const res = await (requestOtp.value
|
||||||
? Auth.auth_login(username.value, password.value, otp.value)
|
? Auth.auth_login(username.value, password.value, otp.value)
|
||||||
: Auth.auth_login(username.value, password.value));
|
: Auth.auth_login(username.value, password.value));
|
||||||
if (isErrorResponse(res)) error.value = "Login failed: " + res.message;
|
if (isErrorResponse(res)) {
|
||||||
else if ("jwt" in res) {
|
message.error(`Login failed: ${res.message}`, {
|
||||||
|
closable: true,
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
} else if ('jwt' in res) {
|
||||||
const root = await FS.get_root(res.jwt);
|
const root = await FS.get_root(res.jwt);
|
||||||
if (isErrorResponse(root)) {
|
if (isErrorResponse(root)) {
|
||||||
error.value = "Get root failed: " + root.message;
|
message.error(`Get root failed: ${root.message}`, {
|
||||||
|
closable: true,
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
jwt.setToken(res.jwt);
|
jwt.setToken(res.jwt);
|
||||||
await router.push({
|
await router.push({
|
||||||
name: "fs",
|
name: 'fs',
|
||||||
params: { node_id: root.rootId },
|
params: { node_id: root.rootId }
|
||||||
});
|
});
|
||||||
} else {
|
} else requestOtp.value = true;
|
||||||
error.value = "";
|
});
|
||||||
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"
|
||||||
|
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" 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>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div>Please input your 2 factor authentication code</div>
|
<n-grid cols="2" x-gap="16" y-gap="16">
|
||||||
<input type="text" placeholder="Code" v-model="otp" />
|
<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>
|
</template>
|
||||||
<button @click="login()">Login</button>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
<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();
|
||||||
@ -18,88 +20,88 @@ onBeforeRouteUpdate(async () => {
|
|||||||
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 === "") {
|
|
||||||
error.value = "Password missing";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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() {
|
|
||||||
const token = await check_token(jwt);
|
const token = await check_token(jwt);
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
await Auth.tfa_disable(token);
|
await Auth.tfa_disable(token);
|
||||||
jwt.logout();
|
jwt.logout();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function tfaEnable() {
|
||||||
|
await router.push('/profile/2fa-enable');
|
||||||
}
|
}
|
||||||
</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">
|
||||||
<div>
|
<n-gi span="2">
|
||||||
<input type="password" placeholder="Old password" v-model="oldPw" />
|
<n-grid cols="2" x-gap="16">
|
||||||
<input type="password" placeholder="New password" v-model="newPw" />
|
<n-gi><UserChangePw /></n-gi>
|
||||||
<input
|
<n-gi>
|
||||||
type="password"
|
<n-card
|
||||||
placeholder="Repeat new password"
|
title="2 Factor authentication"
|
||||||
v-model="newPw2"
|
embedded
|
||||||
/>
|
>
|
||||||
<button @click="changePw">Change</button>
|
<n-button
|
||||||
</div>
|
v-if="user.tfaEnabled"
|
||||||
<div>
|
type="error"
|
||||||
<div>
|
@click="tfaDisable"
|
||||||
2 Factor authentication:
|
>
|
||||||
{{ user.tfaEnabled ? "Enabled" : "Disabled" }}
|
Disable
|
||||||
</div>
|
</n-button>
|
||||||
<div>
|
<n-button
|
||||||
<a href="#" v-if="user.tfaEnabled" @click="tfaDisable"> Disable </a>
|
v-else
|
||||||
<router-link to="/profile/2fa-enable" v-else> Enable </router-link>
|
type="success"
|
||||||
</div>
|
@click="tfaEnable"
|
||||||
</div>
|
>
|
||||||
|
Enable
|
||||||
|
</n-button>
|
||||||
|
</n-card>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-gi>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<n-gi>
|
||||||
<a href="#" @click="logoutAll">Logout everywhere</a>
|
<n-button type="error" @click="logoutAll">
|
||||||
<a href="#" @click="deleteUser">Delete Account</a>
|
Logout everywhere
|
||||||
</div>
|
</n-button>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<n-button type="error" @click="deleteUser">
|
||||||
|
Delete Account
|
||||||
|
</n-button>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div>Loading...</div>
|
<div><n-spin size="small" />Loading...</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -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('');
|
||||||
|
|
||||||
|
const signup = loadingMsgWrapper(message, async () => {
|
||||||
|
if (username.value === '' || password.value === '') {
|
||||||
|
message.error('Email and/or Password missing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (password.value !== password2.value) {
|
if (password.value !== password2.value) {
|
||||||
error.value = "Passwords don't match";
|
message.error("Passwords don't match");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await Auth.auth_signup(username.value, password.value);
|
const res = await Auth.auth_signup(username.value, password.value);
|
||||||
error.value = isErrorResponse(res)
|
if (isErrorResponse(res)) {
|
||||||
? "Signup failed: " + res.message
|
message.error(`Signup failed: ${res.message}`);
|
||||||
: "Signup successful, please wait till an admin unlocks your account.";
|
} 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))
|
||||||
error.value = "Failed to select 2fa type: " + res.message;
|
message.error(`Failed to select 2fa type: ${res.message}`);
|
||||||
else {
|
else currentState.value = state.MAIL;
|
||||||
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))
|
||||||
error.value = "Failed to select 2fa type: " + res.message;
|
message.error(`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;
|
||||||
error.value = "";
|
|
||||||
currentState.value = state.TOTP;
|
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))
|
||||||
error.value = "Failed to submit code: " + res.message;
|
message.error(`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>
|
||||||
|
<n-grid cols="2" x-gap="16" y-gap="16">
|
||||||
<template v-if="currentState === state.SELECT">
|
<template v-if="currentState === state.SELECT">
|
||||||
<div>Select 2 Factor authentication type:</div>
|
<n-gi span="2" style="text-align: center">
|
||||||
<div>
|
Select 2 Factor authentication type
|
||||||
<button @click="selectMail">Mail</button>
|
</n-gi>
|
||||||
<button @click="selectTotp">Google Authenticator</button>
|
<n-gi>
|
||||||
</div>
|
<n-button @click="selectMail">Mail</n-button>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi style="text-align: right">
|
||||||
|
<n-button @click="selectTotp"
|
||||||
|
>Google Authenticator</n-button
|
||||||
|
>
|
||||||
|
</n-gi>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="currentState === state.MAIL">
|
<template v-else-if="currentState === state.MAIL">
|
||||||
<div>Please enter the code you got by mail</div>
|
<n-gi span="2" style="text-align: center">
|
||||||
<input type="text" placeholder="Code" v-model="code" />
|
Please enter the code you got by mail
|
||||||
<button @click="submit()">Submit</button>
|
</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>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<img :src="qrImage" alt="QrCode" />
|
<n-gi span="2" style="text-align: center">
|
||||||
<details>
|
<n-image :src="qrImage" alt="QrCode" />
|
||||||
<summary>Show manual input code</summary>
|
</n-gi>
|
||||||
{{ secret }}
|
<n-gi span="2" style="text-align: center">
|
||||||
</details>
|
<n-popover placement="bottom" trigger="click">
|
||||||
<div>Please enter the current code</div>
|
<template #trigger>
|
||||||
<input type="text" placeholder="Code" v-model="code" />
|
<n-button>Show manual input code</n-button>
|
||||||
<button @click="submit()">Submit</button>
|
|
||||||
</template>
|
</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"
|
||||||
|
Loading…
Reference in New Issue
Block a user