Merge branch 'frontend-redesign' into 'main'

Rewrote the frontend

Closes #13, #2, #16, #21, #7, #18, #19, #8, #9, and #20

See merge request root/fileserver!8
This commit is contained in:
Mutzi 2022-09-03 22:21:32 +00:00
commit ea8330c8c6
98 changed files with 4971 additions and 1796 deletions

View File

@ -7,8 +7,9 @@ stages:
build_backend: build_backend:
stage: build stage: build
cache: cache:
key: backend
paths: paths:
- /root/.cache/vcpkg - vcpkg_cache
script: script:
- apt-get update - apt-get update
- apt-get install g++ gcc make cmake git curl zip unzip tar python3 pkg-config -y - apt-get install g++ gcc make cmake git curl zip unzip tar python3 pkg-config -y
@ -18,18 +19,19 @@ build_backend:
- git clone https://github.com/Microsoft/vcpkg.git . - git clone https://github.com/Microsoft/vcpkg.git .
- ./bootstrap-vcpkg.sh -disableMetrics - ./bootstrap-vcpkg.sh -disableMetrics
- cd $SRC - cd $SRC
- cmake -B build -S backend -DCMAKE_TOOLCHAIN_FILE=$TMP/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release - mkdir -p vcpkg_cache
- VCPKG_DEFAULT_BINARY_CACHE=$SRC/vcpkg_cache cmake -B build -S backend -DCMAKE_TOOLCHAIN_FILE=$TMP/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release
- cmake --build build - cmake --build build
- cp build/backend server - cp build/backend server
artifacts: artifacts:
paths: paths:
- server - server
expire_in: 1h
test_and_build_frontend: test_and_build_frontend:
image: node:latest image: node:latest
stage: build stage: build
cache: cache:
key: frontend
paths: paths:
- frontend/.yarn - frontend/.yarn
- frontend/node_modules - frontend/node_modules
@ -41,7 +43,6 @@ test_and_build_frontend:
artifacts: artifacts:
paths: paths:
- frontend/dist/ - frontend/dist/
expire_in: 1h
package_server: package_server:
stage: package stage: package

9
backend/.idea/cmake.xml Normal file
View 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>

View File

@ -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>

View File

@ -1,4 +1,10 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.21)
if (WIN32 AND (NOT VCPKG_TARGET_TRIPLET))
set(VCPKG_TARGET_TRIPLET x64-windows-static)
endif (WIN32 AND (NOT VCPKG_TARGET_TRIPLET))
#set(VCPKG_LIBRARY_LINKAGE static)
project(backend) project(backend)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
@ -13,13 +19,6 @@ add_executable(backend
src/db/db.h src/db/db.h
src/db/db.cpp src/db/db.cpp
src/db/model/Inode.cc
src/db/model/Inode.h
src/db/model/Tokens.cc
src/db/model/Tokens.h
src/db/model/User.cc
src/db/model/User.h
src/controllers/controllers.h src/controllers/controllers.h
src/controllers/admin.cpp src/controllers/admin.cpp
src/controllers/fs.cpp src/controllers/fs.cpp
@ -32,14 +31,23 @@ add_executable(backend
src/filters/filters.h src/filters/filters.h
src/filters/filters.cpp src/filters/filters.cpp
model/Inode.cc
model/Inode.h
model/Tokens.cc
model/Tokens.h
model/User.cc
model/User.h
SMTPMail-drogon-master/SMTPMail.cc
) )
find_package(Drogon CONFIG REQUIRED) find_package(Drogon CONFIG REQUIRED)
find_package(CURL CONFIG REQUIRED)
find_package(lodepng CONFIG REQUIRED)
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
find_package(OpenCV CONFIG REQUIRED)
find_package(kubazip CONFIG REQUIRED)
find_path(JWT_CPP_INCLUDE_DIRS "jwt-cpp/base.h") find_path(JWT_CPP_INCLUDE_DIRS "jwt-cpp/base.h")
find_path(BOTAN_INCLUDE_DIRS "botan/botan.h") find_path(BOTAN_INCLUDE_DIRS "botan/botan.h")
find_path(QR_INCLUDE_DIRS "qrcodegen.hpp") find_path(QR_INCLUDE_DIRS "qrcodegen.hpp")
@ -48,6 +56,10 @@ find_library(QR_LIBRARY nayuki-qr-code-generator)
target_include_directories(backend PRIVATE target_include_directories(backend PRIVATE
src src
model
shl
SMTPMail-drogon-master
${OpenCV_INCLUDE_DIRS}
${JWT_CPP_INCLUDE_DIRS} ${JWT_CPP_INCLUDE_DIRS}
${BOTAN_INCLUDE_DIRS} ${BOTAN_INCLUDE_DIRS}
${QR_INCLUDE_DIRS} ${QR_INCLUDE_DIRS}
@ -55,14 +67,17 @@ target_include_directories(backend PRIVATE
target_link_libraries(backend target_link_libraries(backend
Drogon::Drogon Drogon::Drogon
CURL::libcurl
lodepng
OpenSSL::SSL OpenSSL::SSL
kubazip::kubazip
${OpenCV_LIBS}
${BOTAN_LIBRARY} ${BOTAN_LIBRARY}
${QR_LIBRARY} ${QR_LIBRARY}
) )
install(TARGETS backend) set_property(TARGET backend PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
install(TARGETS backend RUNTIME_DEPENDENCY_SET backend_deps DESTINATION .)
install(RUNTIME_DEPENDENCY_SET backend_deps)
if(NOT MSVC) if(NOT MSVC)
target_compile_options(backend PRIVATE target_compile_options(backend PRIVATE
@ -74,5 +89,6 @@ else()
endif(NOT MSVC) endif(NOT MSVC)
if(WIN32) if(WIN32)
target_compile_definitions(backend PRIVATE NOMINMAX) target_link_libraries(backend iphlpapi)
target_compile_definitions(backend PRIVATE NOMINMAX _WIN32_WINNT=0x0A00)
endif() endif()

View 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.

View 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.

View 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;
}

View 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
);
};

View File

@ -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;

View File

@ -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)
{ {

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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>

View 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
View 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_

View 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(); });
}

View File

@ -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&) {

View File

@ -5,7 +5,7 @@
#include <botan/base32.h> #include <botan/base32.h>
#include <botan/base64.h> #include <botan/base64.h>
#include <qrcodegen.hpp> #include <qrcodegen.hpp>
#include <lodepng.h> #include <opencv2/opencv.hpp>
#include "controllers/controllers.h" #include "controllers/controllers.h"
#include "db/db.h" #include "db/db.h"
@ -24,35 +24,14 @@ std::string create_totp_qrcode(const db::User& user, const std::string& b32_secr
const int mod_count = code.getSize(); const int mod_count = code.getSize();
const int row_size = qrcode_pixel_size * mod_count; const int row_size = qrcode_pixel_size * mod_count;
std::vector<uint8_t> secret, image, row; cv::Mat image(mod_count, mod_count, CV_8UC1), scaled_image;
row.reserve(row_size); std::vector<uint8_t> image_encoded;
image.reserve(row_size * row_size); for (int y = 0; y < mod_count; y++) for (int x = 0; x < mod_count; x++)
image.at<uint8_t>(x, y) = code.getModule(x, y) ? 0 : 0xff;
cv::resize(image, scaled_image, cv::Size(), qrcode_pixel_size, qrcode_pixel_size, cv::INTER_NEAREST);
cv::imencode(".png", scaled_image, image_encoded);
for (int y = 0; y < mod_count; y++) { return "data:image/png;base64," + Botan::base64_encode(image_encoded);
row.clear();
for (int x = 0; x < mod_count; x++)
row.insert(row.end(), qrcode_pixel_size, code.getModule(x, y) ? 0 : 0xff);
for (int i = 0; i < qrcode_pixel_size; i++)
image.insert(image.end(), row.begin(), row.end());
}
lodepng::encode(secret, image, row_size, row_size, LCT_GREY, 8);
/*
png::image<png::gray_pixel> image(mod_count*qrcode_pixel_size, mod_count*qrcode_pixel_size);
for (int x = 0; x < mod_count; x++) for (int y = 0; y < mod_count; y++) {
const bool mod = code.getModule(x, y);
const int x_img_start = x * qrcode_pixel_size, y_img_start = y * qrcode_pixel_size;
for (int x_img = x_img_start; x_img < x_img_start + qrcode_pixel_size; x_img++) for (int y_img = y_img_start; y_img < y_img_start + qrcode_pixel_size; y_img++)
image[x_img][y_img] = mod ? 0 : 0xff;
}
std::stringstream image_ss;
image.write_stream(image_ss);
std::string image_str = image_ss.str();
std::vector<uint8_t> secret(image_str.data(), image_str.data()+image_str.size());
*/
return "data:image/png;base64," + Botan::base64_encode(secret);
} }
namespace api { namespace api {

View File

@ -17,16 +17,12 @@
#include <jwt-cpp/traits/kazuho-picojson/traits.h> #include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include <jwt-cpp/jwt.h> #include <jwt-cpp/jwt.h>
#include <curl/curl.h> #include <SMTPMail.h>
#include "controllers/controllers.h" #include "controllers/controllers.h"
#include "db/db.h" #include "db/db.h"
#include "dto/dto.h" #include "dto/dto.h"
size_t payload_source(char* ptr, size_t size, size_t nmemb, void* userp) {
auto* ss = (std::stringstream*)userp;
return ss->readsome(ptr, (long)(size*nmemb));
}
namespace api { namespace api {
#if defined(BOTAN_HAS_SYSTEM_RNG) #if defined(BOTAN_HAS_SYSTEM_RNG)
@ -42,33 +38,22 @@ namespace api {
} }
void auth::send_mail(const db::User& user) { void auth::send_mail(const db::User& user) {
std::stringstream ss;
std::time_t t = std::time(nullptr); std::time_t t = std::time(nullptr);
const auto& totp_secret = (const std::vector<uint8_t>&) user.getValueOfTfaSecret(); const auto& totp_secret = (const std::vector<uint8_t>&) user.getValueOfTfaSecret();
char totp[16]; char totp[16];
std::snprintf(totp, 16, "%06d", Botan::TOTP(Botan::OctetString(totp_secret)).generate_totp(t)); std::snprintf(totp, 16, "%06d", Botan::TOTP(Botan::OctetString(totp_secret)).generate_totp(t));
ss.imbue(std::locale("en_US.utf8"));
ss << "Date: " << std::put_time(std::localtime(&t), "%a, %d %b %Y %T %z") << "\r\n";
ss << "To: " << user.getValueOfName() << "\r\n";
ss << "From: fileserver@mattv.de\r\n";
ss << "Message-ID: " << Botan::UUID(*rng).to_string() << "@mattv.de>\r\n";
ss << "Subject: Fileserver - EMail 2fa code\r\n";
ss << "Your code is: " << totp << "\r\n";
ss << "It is valid for 5 Minutes\r\n";
CURL* curl = curl_easy_init(); drogon::app().getPlugin<SMTPMail>()->sendEmail(
curl_easy_setopt(curl, CURLOPT_USERNAME, "no-reply@mattv.de"); "mail.mattv.de",
curl_easy_setopt(curl, CURLOPT_PASSWORD, "noreplyLONGPASS123"); 587,
curl_easy_setopt(curl, CURLOPT_URL, "smtp://mail.mattv.de:587"); "fileserver@mattv.de",
curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL); user.getValueOfName(),
auto recp = curl_slist_append(nullptr, user.getValueOfName().c_str()); "MFileserver - Email 2fa code",
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recp); "Your code is: " + std::string(totp) +"\r\nIt is valid for 5 Minutes",
curl_easy_setopt(curl, CURLOPT_READFUNCTION, &payload_source); "no-reply@mattv.de",
curl_easy_setopt(curl, CURLOPT_READDATA, &ss); "noreplyLONGPASS123",
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1); false
curl_easy_perform(curl); );
curl_slist_free_all(recp);
curl_easy_cleanup(curl);
} }
std::string auth::get_token(const db::User& user) { std::string auth::get_token(const db::User& user) {

View File

@ -1,11 +1,11 @@
#ifndef BACKEND_CONTROLLERS_H #ifndef BACKEND_CONTROLLERS_H
#define BACKEND_CONTROLLERS_H #define BACKEND_CONTROLLERS_H
#include <drogon/drogon.h>
#include <drogon/utils/coroutine.h>
#include <botan/rng.h>
#include <coroutine>
#include <variant> #include <variant>
#include <drogon/drogon.h>
#include <botan/rng.h>
#include <msd/channel.hpp>
#include "db/db.h" #include "db/db.h"
using req_type = const drogon::HttpRequestPtr&; using req_type = const drogon::HttpRequestPtr&;
@ -86,14 +86,31 @@ public:
METHOD_ADD(fs::create_node_req<true>, "/createFile", drogon::Post, "Login"); METHOD_ADD(fs::create_node_req<true>, "/createFile", drogon::Post, "Login");
METHOD_ADD(fs::delete_node_req, "/delete/{}", drogon::Post, "Login"); METHOD_ADD(fs::delete_node_req, "/delete/{}", drogon::Post, "Login");
METHOD_ADD(fs::upload, "/upload/{}", drogon::Post, "Login"); METHOD_ADD(fs::upload, "/upload/{}", drogon::Post, "Login");
METHOD_ADD(fs::create_zip, "/create_zip", drogon::Post, "Login");
METHOD_ADD(fs::download, "/download", drogon::Post, "Login"); METHOD_ADD(fs::download, "/download", drogon::Post, "Login");
METHOD_ADD(fs::download_multi, "/download_multi", drogon::Post, "Login");
METHOD_ADD(fs::download_preview, "/download_preview/{}", drogon::Get, "Login");
METHOD_ADD(fs::get_type, "/get_type/{}", drogon::Get, "Login");
METHOD_LIST_END METHOD_LIST_END
enum class create_node_error {
INVALID_NAME,
INVALID_PARENT,
FILE_PARENT
};
struct mutex_stream {
std::stringstream ss;
std::mutex mutex;
bool done = false;
};
static std::optional<db::INode> get_node(uint64_t node); static std::optional<db::INode> get_node(uint64_t node);
static std::optional<db::INode> get_node_and_validate(const db::User& user, uint64_t node); static std::optional<db::INode> get_node_and_validate(const db::User& user, uint64_t node);
static std::vector<db::INode> get_children(const db::INode& parent); static std::vector<db::INode> get_children(const db::INode& parent);
static std::variant<db::INode, std::string> create_node(std::string name, const db::User& owner, bool file, const std::optional<uint64_t> &parent, bool force = false); static std::variant<db::INode, fs::create_node_error, std::tuple<bool, uint64_t>>
static void delete_node(db::INode node, bool allow_root = false); create_node(std::string name, const db::User& owner, bool file, const std::optional<uint64_t> &parent, bool force = false);
static void delete_node(db::INode node, msd::channel<std::string>& chan, bool allow_root = false);
void root(req_type, cbk_type); void root(req_type, cbk_type);
@ -102,7 +119,11 @@ public:
template<bool file> void create_node_req(req_type req, cbk_type cbk); template<bool file> void create_node_req(req_type req, cbk_type cbk);
void delete_node_req(req_type, cbk_type, uint64_t node); void delete_node_req(req_type, cbk_type, uint64_t node);
void upload(req_type, cbk_type, uint64_t node); void upload(req_type, cbk_type, uint64_t node);
void create_zip(req_type, cbk_type);
void download(req_type, cbk_type); void download(req_type, cbk_type);
void download_multi(req_type, cbk_type);
void download_preview(req_type, cbk_type, uint64_t node);
void get_type(req_type, cbk_type, uint64_t node);
}; };
class user : public drogon::HttpController<user> { class user : public drogon::HttpController<user> {

View File

@ -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,78 @@ 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::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

View File

@ -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());

View File

@ -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";

View File

@ -22,14 +22,24 @@ namespace dto {
namespace Responses { namespace Responses {
struct GetUsersEntry { struct GetUsersEntry {
GetUsersEntry(int id, bool gitlab, bool tfa, std::string name, db::UserRole role) GetUsersEntry(uint64_t id, bool gitlab, bool tfa, std::string name, db::UserRole role)
: id(id), gitlab(gitlab), tfa(tfa), name(std::move(name)), role(role) {} : id(id), gitlab(gitlab), tfa(tfa), name(std::move(name)), role(role) {}
int id; uint64_t id;
bool gitlab, tfa; bool gitlab, tfa;
std::string name; std::string name;
db::UserRole role; db::UserRole role;
}; };
struct GetNodeEntry {
explicit GetNodeEntry(const db::INode& node) : id(node.getValueOfId()), name(node.getValueOfName()), is_file(node.getValueOfIsFile() != 0), has_preview(node.getValueOfHasPreview() != 0), parent(node.getParentId()) {
if (node.getValueOfIsFile() != 0) size = node.getValueOfSize();
}
uint64_t id, size;
std::string name;
bool is_file, has_preview;
std::shared_ptr<uint64_t> parent;
};
drogon::HttpResponsePtr get_error_res(drogon::HttpStatusCode, const std::string &msg); drogon::HttpResponsePtr get_error_res(drogon::HttpStatusCode, const std::string &msg);
drogon::HttpResponsePtr get_success_res(); drogon::HttpResponsePtr get_success_res();
drogon::HttpResponsePtr get_success_res(Json::Value &); drogon::HttpResponsePtr get_success_res(Json::Value &);
@ -46,10 +56,13 @@ namespace dto {
drogon::HttpResponsePtr get_admin_users_res(const std::vector<GetUsersEntry>& users); drogon::HttpResponsePtr get_admin_users_res(const std::vector<GetUsersEntry>& users);
drogon::HttpResponsePtr get_root_res(uint64_t root); drogon::HttpResponsePtr get_root_res(uint64_t root);
drogon::HttpResponsePtr get_node_folder_res(uint64_t id, const std::string& name, const std::shared_ptr<uint64_t>& parent, const std::vector<uint64_t>& children); drogon::HttpResponsePtr get_node_res(const GetNodeEntry& node, const std::vector<GetNodeEntry>& children);
drogon::HttpResponsePtr get_node_file_res(uint64_t id, const std::string& name, const std::shared_ptr<uint64_t>& parent, uint64_t size);
drogon::HttpResponsePtr get_path_res(const std::string& path);
drogon::HttpResponsePtr get_new_node_res(uint64_t id); drogon::HttpResponsePtr get_new_node_res(uint64_t id);
drogon::HttpResponsePtr get_node_exists_res(uint64_t id, bool file);
drogon::HttpResponsePtr get_download_base64_res(const std::string& data);
drogon::HttpResponsePtr get_type_res(const std::string& type);
drogon::HttpResponsePtr get_create_zip_done_res();
drogon::HttpResponsePtr get_create_zip_done_res(uint64_t progress, uint64_t total);
} }
} }

View File

@ -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);
}
} }

View File

@ -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");

View File

@ -2,7 +2,6 @@
#include <fstream> #include <fstream>
#include <drogon/drogon.h> #include <drogon/drogon.h>
#include <curl/curl.h>
#include "dto/dto.h" #include "dto/dto.h"
@ -14,6 +13,9 @@ void cleanup() {
std::cout << "Cleanup up uploads..."; std::cout << "Cleanup up uploads...";
std::filesystem::remove_all("uploads"); std::filesystem::remove_all("uploads");
std::cout << " [Done]" << std::endl; std::cout << " [Done]" << std::endl;
std::cout << "Removing temp folder..." << std::flush;
std::filesystem::remove_all("temp");
std::cout << " [Done]" << std::endl;
std::cout << "Goodbye!" << std::endl; std::cout << "Goodbye!" << std::endl;
} }
@ -38,9 +40,6 @@ int main(int argc, char* argv[]) {
if (std::find(args.begin(), args.end(), "--dev") != args.end()) dev_mode = true; if (std::find(args.begin(), args.end(), "--dev") != args.end()) dev_mode = true;
if (dev_mode) std::cout << "Starting in development mode" << std::endl; if (dev_mode) std::cout << "Starting in development mode" << std::endl;
std::cout << "Setting up..." << std::endl; std::cout << "Setting up..." << std::endl;
std::cout << "Initializing curl..." << std::flush;
curl_global_init(CURL_GLOBAL_ALL);
std::cout << " [Done]" << std::endl;
if (!std::filesystem::exists("files")) { if (!std::filesystem::exists("files")) {
std::cout << "Creating files..." << std::flush; std::cout << "Creating files..." << std::flush;
std::filesystem::create_directory("files"); std::filesystem::create_directory("files");
@ -51,6 +50,15 @@ int main(int argc, char* argv[]) {
std::filesystem::create_directory("logs"); std::filesystem::create_directory("logs");
std::cout << " [Done]" << std::endl; std::cout << " [Done]" << std::endl;
} }
if (std::filesystem::exists("temp")) {
std::cout << "Removing existing temp folder..." << std::flush;
std::filesystem::remove_all("temp");
std::cout << " [Done]" << std::endl;
}
std::cout << "Creating temp folder..." << std::flush;
std::filesystem::create_directory("temp");
std::cout << " [Done]" << std::endl;
auto* loop = drogon::app().getLoop(); auto* loop = drogon::app().getLoop();
loop->queueInLoop([]{ loop->queueInLoop([]{
@ -80,7 +88,8 @@ int main(int argc, char* argv[]) {
" 'name' TEXT,\n" " 'name' TEXT,\n"
" 'parent_id' INTEGER,\n" " 'parent_id' INTEGER,\n"
" 'owner_id' INTEGER NOT NULL,\n" " 'owner_id' INTEGER NOT NULL,\n"
" 'size' INTEGER\n" " 'size' INTEGER,\n"
" 'has_preview' INTEGER NOT NULL\n"
")"); ")");
std::cout << " [Done]" << std::endl; std::cout << " [Done]" << std::endl;
std::cout << "Started!" << std::endl; std::cout << "Started!" << std::endl;
@ -105,8 +114,12 @@ int main(int argc, char* argv[]) {
Json::Value access_logger; Json::Value access_logger;
access_logger["name"] = "drogon::plugin::AccessLogger"; access_logger["name"] = "drogon::plugin::AccessLogger";
Json::Value smtp_mail;
smtp_mail["name"] = "SMTPMail";
Json::Value config; Json::Value config;
config["plugins"].append(access_logger); config["plugins"].append(access_logger);
config["plugins"].append(smtp_mail);
drogon::app() drogon::app()
.setClientMaxBodySize(std::numeric_limits<size_t>::max()) .setClientMaxBodySize(std::numeric_limits<size_t>::max())
@ -127,8 +140,10 @@ int main(int argc, char* argv[]) {
.setIntSignalHandler(cleanup) .setIntSignalHandler(cleanup)
.setTermSignalHandler(cleanup) .setTermSignalHandler(cleanup)
.addListener("0.0.0.0", 5678) .enableRelaunchOnError()
.setThreadNum(2);
.addListener("0.0.0.0", 2345)
.setThreadNum(8);
std::cout << "Setup done!" << std::endl; std::cout << "Setup done!" << std::endl;
drogon::app().run(); drogon::app().run();

View 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" ]
}
]
}

View File

@ -7,11 +7,15 @@
"name": "drogon", "name": "drogon",
"features": ["orm", "sqlite3"] "features": ["orm", "sqlite3"]
}, },
{
"name": "opencv4",
"default-features": false,
"features": ["tiff", "png", "jpeg", "webp", "openexr"]
},
"jwt-cpp", "jwt-cpp",
"botan", "botan",
"curl",
"nayuki-qr-code-generator", "nayuki-qr-code-generator",
"lodepng", "openssl",
"openssl" "kubazip"
] ]
} }

View 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)

View 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()

View File

@ -0,0 +1,4 @@
The package drogon provides CMake targets:
find_package(Drogon CONFIG REQUIRED)
target_link_libraries(main PRIVATE Drogon::Drogon)

View 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"
]
}
}
}

View 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}

View File

@ -0,0 +1,8 @@
{
"default": {
"drogon": {
"baseline": "1.8.0",
"port-version": 0
}
}
}

View File

@ -0,0 +1,9 @@
{
"versions": [
{
"version-semver": "1.8.0",
"port-version": 0,
"path": "$/ports/drogon"
}
]
}

View File

@ -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
View File

@ -0,0 +1,7 @@
{
"tabWidth": 4,
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"endOfLine": "lf"
}

View File

@ -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>

View File

@ -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

View 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

View File

@ -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>

View File

@ -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>

View File

@ -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
); );

View File

@ -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);

View File

@ -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;

View File

@ -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,82 @@ 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 get_type = (
token: string, token: string,
parent: number, node: number
file: File, ): 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);
}

View File

@ -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';

View File

@ -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);

View File

@ -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

View File

@ -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

View 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>

View 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;
}

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,47 @@
import { ref } from 'vue';
import { NProgress } from 'naive-ui';
import filesize from 'filesize';
import { Music, Video, Image } from '@vicons/carbon';
import type { DialogApiInjection } from 'naive-ui/es/dialog/src/DialogProvider';
export default function createBlobDialog(
dialog: DialogApiInjection,
audio: boolean,
video: boolean
) {
const progress = ref(0);
const total = ref(1);
const percentage = ref(0);
const dia = dialog.create({
title:
'Loading ' + (video ? 'video' : audio ? 'audio' : 'image') + '...',
closable: false,
closeOnEsc: false,
maskClosable: false,
icon: () => (video ? <Video /> : audio ? <Music /> : <Image />),
content: () => (
<NProgress
type="line"
percentage={percentage.value}
height={20}
status="info"
showIndicator={false}
/>
),
action: () => (
<div>
{filesize(progress.value, {
base: 2,
standard: 'jedec'
})}
/
{filesize(total.value, {
base: 2,
standard: 'jedec'
})}
- {Math.floor(percentage.value * 1000) / 1000}%
</div>
)
});
return { progress, total, percentage, dia };
}

View File

@ -0,0 +1,126 @@
<script setup lang="ts">
import type { TokenInjectType, Responses } from '@/api';
import { inject, ref, watch } from 'vue';
import { Download, Play } from '@vicons/carbon';
import { useDialog, NGrid, NGi, NButton, NImage, NSpin, NIcon } from 'naive-ui';
import { check_token, FS, isErrorResponse } from '@/api';
import createBlobDialog from '@/components/FileViewer/BlobDownload';
import axios from 'axios';
const props = defineProps<{
node: Responses.GetNode;
}>();
const dialog = useDialog();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
enum fileTypes {
UNKNOWN,
LOADING,
IMAGE,
AUDIO,
VIDEO
}
const fileType = ref<fileTypes>(fileTypes.UNKNOWN);
const src = ref('');
async function download() {
const token = await check_token(jwt);
if (!token) return;
FS.download_file(token, props.node.id);
}
async function loadContent() {
const token = await check_token(jwt);
if (!token) return;
const { progress, total, percentage, dia } = createBlobDialog(
dialog,
fileType.value === fileTypes.AUDIO,
fileType.value === fileTypes.VIDEO
);
total.value = props.node.size ?? 1;
const params = new URLSearchParams();
params.append('jwtToken', token);
params.append('id', props.node.id.toString());
const resp = await axios.post('/api/fs/download', params, {
responseType: 'blob',
onDownloadProgress: (e: ProgressEvent) => {
progress.value = e.loaded;
percentage.value = (e.loaded / e.total) * 100;
}
});
dia.destroy();
if (resp.status != 200) return;
src.value = URL.createObjectURL(resp.data as Blob);
}
async function getType(node: Responses.GetNode) {
fileType.value = fileTypes.LOADING;
if (src.value.startsWith('blob')) URL.revokeObjectURL(src.value);
src.value = '';
const token = await check_token(jwt);
if (!token) return;
const resp = await FS.get_type(token, node.id);
if (isErrorResponse(resp)) return;
if (resp.type.startsWith('image')) {
fileType.value = fileTypes.IMAGE;
await loadContent();
}
if (resp.type.startsWith('audio')) fileType.value = fileTypes.AUDIO;
if (resp.type.startsWith('video')) fileType.value = fileTypes.VIDEO;
}
watch(
() => props.node,
async (to) => {
await getType(to);
if (fileType.value === fileTypes.LOADING)
fileType.value = fileTypes.UNKNOWN;
},
{ immediate: true }
);
</script>
<template>
<n-grid cols="1" x-gap="16" y-gap="16">
<n-gi style="text-align: right">
<n-button @click="download()">
<template #icon>
<n-icon><Download /></n-icon>
</template>
Download
</n-button>
</n-gi>
<n-gi style="text-align: center">
<n-spin v-if="fileType === fileTypes.LOADING" size="large" />
<template v-else-if="fileType !== fileTypes.UNKNOWN">
<video
v-if="fileType === fileTypes.VIDEO && src !== ''"
:src="src"
controls
autoplay
/>
<audio
v-else-if="fileType === fileTypes.AUDIO && src !== ''"
:src="src"
controls
autoplay
/>
<n-image
v-else-if="fileType === fileTypes.IMAGE && src !== ''"
:src="src"
:alt="node.name"
/>
<n-button v-else-if="fileType !== fileTypes.IMAGE" @click="loadContent">
<template #icon>
<n-icon><Play /></n-icon>
</template>
Load and play
</n-button>
</template>
</n-gi>
</n-grid>
</template>
<style scoped></style>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -0,0 +1,203 @@
<script setup lang="ts">
import type { TokenInjectType, Responses, UploadFile } from '@/api';
import { inject, ref } from 'vue';
import { useMessage, NModal, NText, NIcon } from 'naive-ui';
import { CloudUpload } from '@vicons/carbon';
import { FS, check_token, isErrorResponse } from '@/api';
import UploadFileDialog from '@/components/UploadDialog/UploadFileDialog.vue';
import { loadingMsgWrapper } from '@/utils';
const message = useMessage();
const props = defineProps<{
node: Responses.GetNode;
}>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const emit = defineEmits<{
(e: 'reloadNode'): void;
}>();
const uploadArea = ref<HTMLDivElement>();
const fileInput = ref<HTMLInputElement>();
const uploadDialog = ref();
const uploadDialogShow = ref(false);
const files = ref<UploadFile[]>([]);
function startDrag() {
uploadArea.value?.classList.add('uploadActive');
}
function stopDrag() {
uploadArea.value?.classList.remove('uploadActive');
}
function openBrowser() {
fileInput.value?.click();
}
function browserChanged(event: Event) {
files.value = Array.from(
(event.target as HTMLInputElement).files ?? []
).map((file) => {
return {
parent: props.node.id,
fullName: file.name,
file
};
});
uploadFiles();
}
interface FileSystemDirectoryReader {
readEntries(
successCallback: (entries: FileSystemEntry[]) => void,
errorCallback?: (err: DOMException) => void
): void;
}
interface FileSystemEntry {
readonly fullPath: string;
readonly isDirectory: boolean;
readonly isFile: boolean;
readonly name: string;
file(
successCallback: (file: File) => void,
errorCallback?: (err: DOMException) => void
): void;
createReader(): FileSystemDirectoryReader;
}
const asyncReadEntries = async (
reader: FileSystemDirectoryReader
): Promise<FileSystemEntry[]> =>
new Promise((resolve, reject) => reader.readEntries(resolve, reject));
const getFile = async (entry: FileSystemEntry): Promise<File> =>
new Promise((resolve, reject) => entry.file(resolve, reject));
async function processDirOrFile(
entry: FileSystemEntry,
parent: number,
token: string
) {
if (entry.isDirectory) {
const resp = await FS.create_folder(token, parent, entry.name);
if (isErrorResponse(resp)) return;
if ('exists' in resp && resp.isFile) return;
const reader = entry.createReader();
let entries = [];
do {
try {
entries = await asyncReadEntries(reader);
entries.forEach((e) => processDirOrFile(e, resp.id, token));
} catch {
break;
}
} while (entries.length != 0);
} else
files.value.push({
parent: parent,
fullName: entry.fullPath.slice(1),
file: await getFile(entry)
});
}
const filesDropped = loadingMsgWrapper(message, async (event: DragEvent) => {
stopDrag();
if (!event.dataTransfer) return;
const token = await check_token(jwt);
if (!token) return;
files.value = [];
for (const file of event.dataTransfer.items) {
const entry = file.webkitGetAsEntry();
if (entry)
await processDirOrFile(
entry as unknown as FileSystemEntry,
props.node.id,
token
);
}
uploadFiles();
});
function uploadFiles() {
if (files.value.length == 0) return;
uploadDialogShow.value = true;
}
async function uploadFilesDialogOpen() {
await uploadDialog.value?.startUpload();
uploadDialogShow.value = false;
if (fileInput.value) fileInput.value.value = '';
emit('reloadNode');
}
</script>
<template>
<div
class="uploadArea"
ref="uploadArea"
@drop.prevent
@dragenter.prevent
@dragover.prevent
@dragleave.prevent
@dragend.prevent
@click="openBrowser"
@drop="filesDropped"
@dragenter="startDrag"
@dragover="startDrag"
@dragleave="stopDrag"
@dragend="stopDrag"
>
<input type="file" ref="fileInput" multiple @input="browserChanged" />
<div>
<n-icon size="2em">
<CloudUpload />
</n-icon>
</div>
<n-text>
Click&nbsp;or&nbsp;drag&nbsp;here&nbsp;to&nbsp;upload&nbsp;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>

View File

@ -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>

View 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>

View File

@ -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[];
}
}

View File

@ -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 {}

View File

@ -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;
}

View File

@ -1 +0,0 @@
export class BaseRequest {}

View File

@ -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 {}

View File

@ -1,4 +0,0 @@
export * from "./base";
export * as Auth from "./auth";
export * as FS from "./fs";
export * as Admin from "./admin";

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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;
}

View File

@ -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 {}

View File

@ -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";

View File

@ -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 {}

View File

@ -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);
}
};
}

View File

@ -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');

View File

@ -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
View 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();
}
});
}

View File

@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@ -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();
} });
};
async function logoutUser(user: number) { const logoutUser = action(Admin.logout);
const token = await check_token(jwt); const disableTfa = action(Admin.disable_tfa);
if (!token) return; const deleteUser = action(Admin.delete_user);
const res = await Admin.logout(user, token); const selectOptions: SelectOption[] = [
if (isErrorResponse(res)) console.error(res.message); {
await updatePanel(); label: 'Disabled',
} value: 0
},
{
label: 'User',
value: 1
},
{
label: 'Admin',
value: 2
}
];
async function deleteUser(user: number) { const columns: DataTableColumn<Responses.GetUsersEntry>[] = [
const token = await check_token(jwt); {
if (!token) return; title: 'Name',
key: 'name'
const res = await Admin.delete_user(user, token); },
if (isErrorResponse(res)) console.error(res.message); {
await updatePanel(); title: 'Type',
} key: 'gitlab',
render(user) {
return user.gitlab ? 'Gitlab' : 'Password';
}
},
{
title: 'Role',
key: 'role',
minWidth: 120,
render(user) {
return (
<NSelect
value={user.role}
options={selectOptions}
onUpdateValue={(value: number) => setRole(user.id, value)}
/>
);
}
},
{
title: 'Tfa Status',
key: 'tfaEnabled',
render(user) {
return user.gitlab ? '' : user.tfaEnabled ? 'Enabled' : 'Disabled';
}
},
{
title: 'Actions',
key: 'actions',
render(user) {
return (
<NButtonGroup>
<NButton onClick={() => logoutUser(user.id)}>
Logout all
</NButton>
{user.tfaEnabled ? (
<NButton
type="warning"
onClick={() => disableTfa(user.id)}
>
Disable Tfa
</NButton>
) : (
''
)}
<NButton onClick={() => deleteUser(user.id)} type="error">
Delete
</NButton>
</NButtonGroup>
);
}
}
];
</script> </script>
<template> <template>
<table> <n-data-table :columns="columns" :data="users" />
<tr>
<th>Name</th>
<th>Type</th>
<th>Role</th>
<th>Tfa Status</th>
<th>Actions</th>
</tr>
<tr v-for="user in users" :key="user.id">
<td>{{ user.name }}</td>
<td>{{ user.gitlab ? "Gitlab" : "Password" }}</td>
<td>
<select @change="setRole(user.id, ($event.target as HTMLSelectElement).value)">
<option value="0" :selected="user.role === 0 ? true : undefined">
Disabled
</option>
<option value="1" :selected="user.role === 1 ? true : undefined">
User
</option>
<option value="2" :selected="user.role === 2 ? true : undefined">
Admin
</option>
</select>
</td>
<td v-if="user.gitlab"></td>
<td v-else>
{{ user.tfaEnabled ? "Enabled" : "Disabled" }}
</td>
<td>
<button v-if="user.tfaEnabled" @click="disableTfa(user.id)">
Disable Tfa
</button>
<button @click="logoutUser(user.id)">Logout all</button>
<button @click="deleteUser(user.id)">Delete</button>
</td>
</tr>
</table>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -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>

View File

@ -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 }
}); });
} }

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
}
}); });

View 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';
}
}
}
}
}
});

View File

@ -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"