Rewrote backend in c++

This commit is contained in:
Mutzi 2022-08-28 17:37:09 +02:00
parent d199ecae87
commit 2e8877837a
98 changed files with 14078 additions and 1433 deletions

2
.gitignore vendored
View File

@ -115,3 +115,5 @@ fabric.properties
.idea/**/azureSettings.xml
# End of https://www.toptal.com/developers/gitignore/api/clion
run/

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
backend

19
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<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">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/run/sqlite.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

2
.idea/file_server.iml Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module classpath="CMake" type="CPP_MODULE" version="4" />

11
.idea/misc.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$/backend">
<contentRoot DIR="$PROJECT_DIR$" />
</component>
<component name="CidrRootsConfiguration">
<libraryRoots>
<file path="$PROJECT_DIR$/backend/lib" />
</libraryRoots>
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/file_server.iml" filepath="$PROJECT_DIR$/.idea/file_server.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

70
backend/CMakeLists.txt Normal file
View File

@ -0,0 +1,70 @@
cmake_minimum_required(VERSION 3.20)
project(backend)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
add_executable(backend
src/main.cpp
src/dto/dto.h
src/dto/responses.cpp
src/db/db.h
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/admin.cpp
src/controllers/fs.cpp
src/controllers/user.cpp
src/controllers/auth/auth_common.cpp
src/controllers/auth/auth_basic.cpp
src/controllers/auth/auth_2fa.cpp
src/filters/filters.h
src/filters/filters.cpp
src/controllers/auth/auth_gitlab.cpp)
if (MINGW)
target_link_libraries(backend -static-libgcc -static-libstdc++)
endif (MINGW)
find_package(Drogon CONFIG REQUIRED)
find_package(CURL CONFIG REQUIRED)
find_package(PNG REQUIRED)
find_path(JWT_CPP_INCLUDE_DIRS "jwt-cpp/base.h")
find_path(BOTAN_INCLUDE_DIRS "botan/botan.h")
find_path(QR_INCLUDE_DIRS "qrcodegen.hpp")
find_path(PNGPP_INCLUDE_DIRS "png++/color.hpp")
find_library(BOTAN_LIBRARY botan-2)
find_library(QR_LIBRARY nayuki-qr-code-generator)
target_include_directories(backend PRIVATE
src
${JWT_CPP_INCLUDE_DIRS}
${BOTAN_INCLUDE_DIRS}
${QR_INCLUDE_DIRS}
${PNGPP_INCLUDE_DIRS}
)
target_link_libraries(backend
Drogon::Drogon
CURL::libcurl
PNG::PNG
${BOTAN_LIBRARY}
${QR_LIBRARY}
)
target_compile_options(backend PRIVATE
$<$<CONFIG:Debug>:-g -Wall -Wno-unknown-pragmas>
$<$<CONFIG:Release>:-O3>
)

View File

@ -0,0 +1,88 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "performance-unnecessary-value-param"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include "controllers.h"
#include "dto/dto.h"
namespace api {
void admin::users(req_type, cbk_type cbk) {
db::MapperUser user_mapper(drogon::app().getDbClient());
std::vector<dto::Responses::GetUsersEntry> entries;
auto users = user_mapper.findAll();
for (const db::User& user : users)
entries.emplace_back(
user.getValueOfId(),
user.getValueOfGitlab() != 0,
db::User_getEnumTfaType(user) != db::tfaTypes::NONE,
user.getValueOfName(),
db::User_getEnumRole(user)
);
cbk(dto::Responses::get_admin_users_res(entries));
}
void admin::set_role(req_type req, cbk_type cbk) {
Json::Value& json = *req->jsonObject();
try {
uint64_t user_id = dto::json_get<uint64_t>(json, "user").value();
db::UserRole role = (db::UserRole)dto::json_get<int>(json, "role").value();
db::MapperUser user_mapper(drogon::app().getDbClient());
auto user = user_mapper.findByPrimaryKey(user_id);
user.setRole(role);
user_mapper.update(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void admin::logout(req_type req, cbk_type cbk) {
Json::Value& json = *req->jsonObject();
try {
uint64_t user_id = dto::json_get<uint64_t>(json, "user").value();
db::MapperUser user_mapper(drogon::app().getDbClient());
auto user = user_mapper.findByPrimaryKey(user_id);
auth::revoke_all(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void admin::delete_user(req_type req, cbk_type cbk) {
Json::Value& json = *req->jsonObject();
try {
uint64_t user_id = dto::json_get<uint64_t>(json, "user").value();
db::MapperUser user_mapper(drogon::app().getDbClient());
auto user = user_mapper.findByPrimaryKey(user_id);
auth::revoke_all(user);
fs::delete_node(fs::get_node(user.getValueOfRootId()).value(), true);
user_mapper.deleteOne(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void admin::disable_2fa(req_type req, cbk_type cbk) {
Json::Value& json = *req->jsonObject();
try {
uint64_t user_id = dto::json_get<uint64_t>(json, "user").value();
db::MapperUser user_mapper(drogon::app().getDbClient());
auto user = user_mapper.findByPrimaryKey(user_id);
user.setTfaType(db::tfaTypes::NONE);
user_mapper.update(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,103 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "readability-make-member-function-const"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include <botan/base32.h>
#include <botan/base64.h>
#include <qrcodegen.hpp>
#include <png++/png.hpp>
#include "controllers/controllers.h"
#include "db/db.h"
#include "dto/dto.h"
std::string create_totp_qrcode(const db::User& user, const std::string& b32_secret) {
const int qrcode_pixel_size = 4;
std::stringstream code_ss;
code_ss << "otpauth://totp/MFileserver:"
<< user.getValueOfName()
<< "?secret="
<< b32_secret
<< "&issuer=MFileserver";
auto code = qrcodegen::QrCode::encodeText(code_ss.str().c_str(), qrcodegen::QrCode::Ecc::MEDIUM);
const int mod_count = code.getSize();
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 {
void auth::tfa_setup(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
Json::Value &json = *req->jsonObject();
try {
bool mail = dto::json_get<bool>(json, "mail").value();
auto secret_uchar = rng->random_vec(32);
std::vector<char> secret(secret_uchar.data(), secret_uchar.data()+32);
user.setTfaSecret(secret);
db::MapperUser user_mapper(drogon::app().getDbClient());
user_mapper.update(user);
if (mail) {
send_mail(user);
cbk(dto::Responses::get_success_res());
} else {
std::string b32_secret = Botan::base32_encode(secret_uchar);
b32_secret.erase(std::remove(b32_secret.begin(), b32_secret.end(), '='), b32_secret.end());
std::string code = create_totp_qrcode(user, b32_secret);
cbk(dto::Responses::get_tfa_setup_res(b32_secret, code));
}
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void auth::tfa_complete(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
Json::Value &json = *req->jsonObject();
try {
bool mail = dto::json_get<bool>(json, "mail").value();
uint32_t code = std::stoi(dto::json_get<std::string>(json, "code").value());
user.setTfaType(mail ? db::tfaTypes::EMAIL : db::tfaTypes::TOTP);
if (!verify2fa(user, code))
return cbk(dto::Responses::get_unauth_res("Incorrect 2fa"));
db::MapperUser user_mapper(drogon::app().getDbClient());
user_mapper.update(user);
revoke_all(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void auth::tfa_disable(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
db::MapperUser user_mapper(drogon::app().getDbClient());
user.setTfaType(db::tfaTypes::NONE);
user_mapper.update(user);
revoke_all(user);
cbk(dto::Responses::get_success_res());
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,135 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "readability-make-member-function-const"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include <botan/argon2.h>
#include <botan/totp.h>
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include <jwt-cpp/jwt.h>
#include "controllers/controllers.h"
#include "db/db.h"
#include "dto/dto.h"
namespace api {
void auth::login(req_type req, cbk_type cbk) {
Json::Value &json = *req->jsonObject();
try {
std::string username = dto::json_get<std::string>(json, "username").value();
std::string password = dto::json_get<std::string>(json, "password").value();
std::optional<std::string> otp = dto::json_get<std::string>(json, "otp");
auto db = drogon::app().getDbClient();
db::MapperUser user_mapper(db);
auto db_users = user_mapper.findBy(
db::Criteria(db::User::Cols::_name, db::CompareOps::EQ, username) &&
db::Criteria(db::User::Cols::_gitlab, db::CompareOps::EQ, 0)
);
if (db_users.empty()) {
cbk(dto::Responses::get_unauth_res("Invalid username or password"));
return;
}
db::User &db_user = db_users.at(0);
if (!Botan::argon2_check_pwhash(password.c_str(), password.size(), db_user.getValueOfPassword())) {
cbk(dto::Responses::get_unauth_res("Invalid username or password"));
return;
}
if (db::User_getEnumRole(db_user) == db::UserRole::DISABLED) {
cbk(dto::Responses::get_unauth_res("Account is disabled"));
return;
}
const auto tfa = db::User_getEnumTfaType(db_user);
if (tfa != db::tfaTypes::NONE) {
if (!otp.has_value()) {
if (tfa == db::tfaTypes::EMAIL) send_mail(db_user);
return cbk(dto::Responses::get_success_res());
}
if (!verify2fa(db_user, std::stoi(otp.value())))
return cbk(dto::Responses::get_unauth_res("Incorrect 2fa"));
}
cbk(dto::Responses::get_login_res(get_token(db_user)));
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void auth::signup(req_type req, cbk_type cbk) {
Json::Value &json = *req->jsonObject();
try {
std::string username = dto::json_get<std::string>(json, "username").value();
std::string password = dto::json_get<std::string>(json, "password").value();
db::MapperUser user_mapper(drogon::app().getDbClient());
auto existing_users = user_mapper.count(
db::Criteria(db::User::Cols::_name, db::CompareOps::EQ, username) &&
db::Criteria(db::User::Cols::_gitlab, db::CompareOps::EQ, 0)
);
if (existing_users != 0) {
cbk(dto::Responses::get_badreq_res("Username is already taken"));
return;
}
//std::string hash = Botan::argon2_generate_pwhash(password.c_str(), password.size(), *rng, 1, 256*1024, 2);
std::string hash = Botan::argon2_generate_pwhash(password.c_str(), password.size(), *rng, 1, 16*1024, 1);
db::User new_user;
new_user.setName(username);
new_user.setPassword(hash);
new_user.setGitlab(0);
new_user.setRole(db::UserRole::DISABLED);
new_user.setRootId(0);
new_user.setTfaType(db::tfaTypes::NONE);
user_mapper.insert(new_user);
generate_root(new_user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception& e) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void auth::refresh(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
db::Token token = dto::get_token(req);
db::MapperToken token_mapper(drogon::app().getDbClient());
token_mapper.deleteOne(token);
cbk(dto::Responses::get_login_res( get_token(user)));
}
void auth::logout_all(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
revoke_all(user);
cbk(dto::Responses::get_success_res());
}
void auth::change_password(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
Json::Value &json = *req->jsonObject();
try {
std::string old_pw = dto::json_get<std::string>(json, "oldPassword").value();
std::string new_pw = dto::json_get<std::string>(json, "newPassword").value();
auto db = drogon::app().getDbClient();
db::MapperUser user_mapper(db);
if (!Botan::argon2_check_pwhash(old_pw.c_str(), old_pw.size(), user.getValueOfPassword()))
return cbk(dto::Responses::get_unauth_res("Old password is wrong"));
std::string hash = Botan::argon2_generate_pwhash(new_pw.c_str(), new_pw.size(), *rng, 1, 256*1024, 2);
user.setPassword(hash);
user_mapper.update(user);
revoke_all(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,110 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "readability-make-member-function-const"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include <chrono>
#include <iomanip>
#include <botan/argon2.h>
#include <botan/uuid.h>
#include <botan/totp.h>
#if defined(BOTAN_HAS_SYSTEM_RNG)
#include <botan/system_rng.h>
#else
#include <botan/auto_rng.h>
#endif
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include <jwt-cpp/jwt.h>
#include <curl/curl.h>
#include "controllers/controllers.h"
#include "db/db.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 {
#if defined(BOTAN_HAS_SYSTEM_RNG)
std::unique_ptr<Botan::RNG> auth::rng = std::make_unique<Botan::System_RNG>();
#else
std::unique_ptr<Botan::RNG> auth::rng = std::make_unique<Botan::AutoSeeded_RNG>();
#endif
bool auth::verify2fa(const db::User& user, uint32_t totp) {
size_t allowed_skew = db::User_getEnumTfaType(user) == db::tfaTypes::TOTP ? 0 : 10;
const auto& totp_secret = (const std::vector<uint8_t>&) user.getValueOfTfaSecret();
return Botan::TOTP(Botan::OctetString(totp_secret)).verify_totp(totp, std::chrono::system_clock::now(), allowed_skew);
}
void auth::send_mail(const db::User& user) {
std::stringstream ss;
std::time_t t = std::time(nullptr);
const auto& totp_secret = (const std::vector<uint8_t>&) user.getValueOfTfaSecret();
char totp[16];
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();
curl_easy_setopt(curl, CURLOPT_USERNAME, "no-reply@mattv.de");
curl_easy_setopt(curl, CURLOPT_PASSWORD, "noreplyLONGPASS123");
curl_easy_setopt(curl, CURLOPT_URL, "smtp://mail.mattv.de:587");
curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
auto recp = curl_slist_append(nullptr, user.getValueOfName().c_str());
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recp);
curl_easy_setopt(curl, CURLOPT_READFUNCTION, &payload_source);
curl_easy_setopt(curl, CURLOPT_READDATA, &ss);
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
curl_easy_perform(curl);
curl_slist_free_all(recp);
curl_easy_cleanup(curl);
}
std::string auth::get_token(const db::User& user) {
auto db = drogon::app().getDbClient();
db::MapperToken token_mapper(db);
const auto iat = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch());
const auto exp = iat + std::chrono::hours{24};
db::Token new_token;
new_token.setOwnerId(user.getValueOfId());
new_token.setExp(exp.count());
token_mapper.insert(new_token);
return jwt::create<jwt::traits::kazuho_picojson>()
.set_type("JWT")
.set_payload_claim("sub", picojson::value((int64_t)user.getValueOfId()))
.set_payload_claim("jti", picojson::value((int64_t)new_token.getValueOfId()))
.set_issued_at(std::chrono::system_clock::from_time_t(iat.count()))
.set_expires_at(std::chrono::system_clock::from_time_t(exp.count()))
.sign(jwt::algorithm::hs256{jwt_secret});
}
void auth::generate_root(db::User& user) {
db::MapperUser user_mapper(drogon::app().getDbClient());
auto node = fs::create_node("", user, false, std::nullopt, true);
user.setRootId(std::get<db::INode>(node).getValueOfId());
user_mapper.update(user);
}
void auth::revoke_all(const db::User& user) {
db::MapperToken token_mapper(drogon::app().getDbClient());
token_mapper.deleteBy(db::Criteria(db::Token::Cols::_owner_id, db::CompareOps::EQ, user.getValueOfId()));
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,118 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "performance-unnecessary-value-param"
#pragma ide diagnostic ignored "readability-make-member-function-const"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include <utility>
#include "controllers/controllers.h"
#include "dto/dto.h"
const std::string GITLAB_ID = "98bcbad78cb1f880d1d1de62291d70a791251a7bea077bfe7df111ef3c115760";
const std::string GITLAB_SECRET = "7ee01d2b204aff3a05f9d028f004d169b6d381ec873e195f314b3935fa150959";
const std::string GITLAB_URL = "https://gitlab.mattv.de";
const std::string GITLAB_API_URL = "https://ssh.gitlab.mattv.de";
std::string get_redirect_uri(req_type req) {
auto host_header = req->headers().find("host");
std::stringstream ss;
ss << (req->isOnSecureConnection() ? "https" : "http")
<< "://"
<< (host_header != req->headers().end() ? host_header->second : "127.0.0.1:1234")
<< "/api/auth/gitlab_callback";
return drogon::utils::urlEncode(ss.str());
}
const drogon::HttpClientPtr& get_gitlab_client() {
static drogon::HttpClientPtr client = drogon::HttpClient::newHttpClient(GITLAB_API_URL, drogon::app().getLoop(), false, false);
return client;
}
namespace api {
std::optional<auth::gitlab_tokens> auth::get_gitlab_tokens(req_type req, const std::string& code_or_token, bool token) {
std::stringstream ss;
ss << "/oauth/token"
<< "?redirect_uri=" << get_redirect_uri(req)
<< "&client_id=" << GITLAB_ID
<< "&client_secret=" << GITLAB_SECRET
<< (token ? "&refresh_token=" : "&code=") << code_or_token
<< "&grant_type=" << (token ? "refresh_token" : "authorization_code");
auto gitlab_req = drogon::HttpRequest::newHttpRequest();
gitlab_req->setPathEncode(false);
gitlab_req->setPath(ss.str());
gitlab_req->setMethod(drogon::HttpMethod::Post);
auto res_tuple = get_gitlab_client()->sendRequest(gitlab_req);
auto res = res_tuple.second;
if ((res->statusCode() != drogon::HttpStatusCode::k200OK) && (res->statusCode() != drogon::HttpStatusCode::k201Created))
return std::nullopt;
auto json = *res->jsonObject();
return std::make_optional<gitlab_tokens>(
json["access_token"].as<std::string>(),
json["refresh_token"].as<std::string>()
);
}
std::optional<auth::gitlab_user> auth::get_gitlab_user(const std::string& at) {
auto gitlab_req = drogon::HttpRequest::newHttpRequest();
gitlab_req->setPath("/api/v4/user");
gitlab_req->addHeader("Authorization", "Bearer " + at);
gitlab_req->setMethod(drogon::HttpMethod::Get);
auto res_tuple = get_gitlab_client()->sendRequest(gitlab_req);
auto res = res_tuple.second;
if (res->statusCode() != drogon::HttpStatusCode::k200OK)
return std::nullopt;
auto json = *res->jsonObject();
return std::make_optional<gitlab_user>(
json["username"].as<std::string>(),
json.get("is_admin", false).as<bool>()
);
}
void auth::gitlab(req_type req, cbk_type cbk) {
std::stringstream ss;
ss << GITLAB_URL << "/oauth/authorize"
<< "?redirect_uri=" << get_redirect_uri(req)
<< "&client_id=" << GITLAB_ID
<< "&scope=read_user&response_type=code";
cbk(drogon::HttpResponse::newRedirectionResponse(ss.str()));
}
void auth::gitlab_callback(req_type req, cbk_type cbk, std::string code) {
auto tokens = get_gitlab_tokens(req, code, false);
if (!tokens.has_value())
return cbk(dto::Responses::get_unauth_res("Invalid code"));
auto info = get_gitlab_user(tokens->at);
if (!info.has_value())
return cbk(dto::Responses::get_unauth_res("Invalid code"));
db::MapperUser user_mapper(drogon::app().getDbClient());
auto db_users = user_mapper.findBy(
db::Criteria(db::User::Cols::_name, db::CompareOps::EQ, info->name) &&
db::Criteria(db::User::Cols::_gitlab, db::CompareOps::EQ, 1)
);
if (db_users.empty()) {
db::User new_user;
new_user.setName(info->name);
new_user.setPassword("");
new_user.setGitlab(1);
new_user.setRole(info->is_admin ? db::UserRole::ADMIN : db::UserRole::DISABLED);
new_user.setRootId(0);
new_user.setTfaType(db::tfaTypes::NONE);
user_mapper.insert(new_user);
generate_root(new_user);
db_users.push_back(new_user);
}
db::User& db_user = db_users.at(0);
db_user.setGitlabAt(tokens->at);
db_user.setGitlabRt(tokens->rt);
user_mapper.update(db_user);
const std::string& token = get_token(db_user);
cbk(drogon::HttpResponse::newRedirectionResponse("/set_token?token="+token));
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,119 @@
#ifndef 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 "db/db.h"
using req_type = const drogon::HttpRequestPtr&;
using cbk_type = std::function<void(const drogon::HttpResponsePtr &)>&&;
namespace api {
class admin : public drogon::HttpController<admin> {
public:
METHOD_LIST_BEGIN
METHOD_ADD(admin::users, "/users", drogon::Get, "Login", "Admin");
METHOD_ADD(admin::set_role, "/set_role", drogon::Post, "Login", "Admin");
METHOD_ADD(admin::logout, "/logout", drogon::Post, "Login", "Admin");
METHOD_ADD(admin::delete_user, "/delete", drogon::Post, "Login", "Admin");
METHOD_ADD(admin::disable_2fa, "/disable_2fa", drogon::Post, "Login", "Admin");
METHOD_LIST_END
void users(req_type, cbk_type);
void set_role(req_type, cbk_type);
void logout(req_type, cbk_type);
void delete_user(req_type, cbk_type);
void disable_2fa(req_type, cbk_type);
};
class auth : public drogon::HttpController<auth> {
public:
METHOD_LIST_BEGIN
METHOD_ADD(auth::gitlab, "/gitlab", drogon::Get);
METHOD_ADD(auth::gitlab_callback, "/gitlab_callback?code={}", drogon::Get);
METHOD_ADD(auth::signup, "/signup", drogon::Post);
METHOD_ADD(auth::login, "/login", drogon::Post);
METHOD_ADD(auth::refresh, "/refresh", drogon::Post, "Login");
METHOD_ADD(auth::tfa_setup, "/2fa/setup", drogon::Post, "Login");
METHOD_ADD(auth::tfa_complete, "/2fa/complete", drogon::Post, "Login");
METHOD_ADD(auth::tfa_disable, "/2fa/disable", drogon::Post, "Login");
METHOD_ADD(auth::change_password, "/change_password", drogon::Post, "Login");
METHOD_ADD(auth::logout_all, "/logout_all", drogon::Post, "Login");
METHOD_LIST_END
struct gitlab_tokens {
gitlab_tokens(std::string at, std::string rt) : at(std::move(at)), rt(std::move(rt)) {}
std::string at, rt;
};
struct gitlab_user {
gitlab_user(std::string name, bool isAdmin) : name(std::move(name)), is_admin(isAdmin) {}
std::string name;
bool is_admin;
};
static std::unique_ptr<Botan::RNG> rng;
static std::optional<gitlab_tokens> get_gitlab_tokens(req_type, const std::string&, bool token);
static std::optional<gitlab_user> get_gitlab_user(const std::string&);
static bool verify2fa(const db::User&, uint32_t totp);
static void send_mail(const db::User&);
static std::string get_token(const db::User&);
static void generate_root(db::User&);
static void revoke_all(const db::User&);
void gitlab(req_type, cbk_type);
void gitlab_callback(req_type, cbk_type, std::string code);
void signup(req_type, cbk_type);
void login(req_type, cbk_type);
void refresh(req_type, cbk_type);
void tfa_setup(req_type, cbk_type);
void tfa_complete(req_type, cbk_type);
void tfa_disable(req_type, cbk_type);
void change_password(req_type, cbk_type);
void logout_all(req_type, cbk_type);
};
class fs : public drogon::HttpController<fs> {
public:
METHOD_LIST_BEGIN
METHOD_ADD(fs::root, "/root", drogon::Get, "Login");
METHOD_ADD(fs::node, "/node/{}", drogon::Get, "Login");
METHOD_ADD(fs::path, "/path/{}", drogon::Get, "Login");
METHOD_ADD(fs::create_node_req<false>, "/createFolder", 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::upload, "/upload/{}", drogon::Post, "Login");
METHOD_ADD(fs::download, "/download", drogon::Post, "Login");
METHOD_LIST_END
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::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 void delete_node(db::INode node, bool allow_root = false);
void root(req_type, cbk_type);
void node(req_type, cbk_type, uint64_t node);
void path(req_type, cbk_type, uint64_t node);
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 upload(req_type, cbk_type, uint64_t node);
void download(req_type, cbk_type);
};
class user : public drogon::HttpController<user> {
public:
METHOD_LIST_BEGIN
METHOD_ADD(user::info, "/info", drogon::Get, "Login");
METHOD_ADD(user::delete_user, "/delete", drogon::Post, "Login");
METHOD_LIST_END
void info(req_type, cbk_type);
void delete_user(req_type, cbk_type);
};
}
#endif //BACKEND_CONTROLLERS_H

View File

@ -0,0 +1,211 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "performance-unnecessary-value-param"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include <filesystem>
#include "controllers.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<>:\"/\\|";
std::string generate_path(db::INode node) {
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);
}
std::stringstream ss;
while (!path.empty()) {
const db::INode& seg = path.top();
ss << seg.getValueOfName();
if (seg.getValueOfIsFile() == 0) ss << '/';
path.pop();
}
return ss.str();
}
namespace api {
std::optional<db::INode> fs::get_node(uint64_t node) {
db::MapperInode inode_mapper(drogon::app().getDbClient());
try {
return inode_mapper.findByPrimaryKey(node);
} catch (const std::exception&) {
return std::nullopt;
}
}
std::optional<db::INode> fs::get_node_and_validate(const db::User &user, uint64_t node) {
auto inode = get_node(node);
if (!inode.has_value()) return std::nullopt;
if (inode->getValueOfOwnerId() != user.getValueOfId()) return std::nullopt;
return inode;
}
std::vector<db::INode> fs::get_children(const db::INode& parent) {
db::MapperInode inode_mapper(drogon::app().getDbClient());
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) {
// Stolen from https://github.com/boostorg/filesystem/blob/develop/src/portability.cpp
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 == "..")
return {"Invalid name"};
db::INode node;
node.setIsFile(file ? 1 : 0);
node.setName(name);
node.setOwnerId(owner.getValueOfId());
if (parent.has_value()) {
auto parent_node = get_node_and_validate(owner, *parent);
if (!parent_node.has_value())
return {"Invalid parent"};
if (parent_node->getValueOfIsFile() != 0)
return {"Can't use file as parent"};
auto children = get_children(*parent_node);
for (const auto& child : children)
if (child.getValueOfName() == name)
return {"File/Folder already exists"};
node.setParentId(*parent);
}
db::MapperInode inode_mapper(drogon::app().getDbClient());
inode_mapper.insert(node);
return {node};
}
void fs::delete_node(db::INode node, bool allow_root) {
if (node.getValueOfParentId() == 0 && (!allow_root)) return;
if (node.getValueOfIsFile() == 0) {
auto children = get_children(node);
for (const auto& child : children) delete_node(child, false);
} else {
std::filesystem::path p("./files");
p /= std::to_string(node.getValueOfId());
std::filesystem::remove(p);
}
db::MapperInode inode_mapper(drogon::app().getDbClient());
inode_mapper.deleteOne(node);
}
void fs::root(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
cbk(dto::Responses::get_root_res(user.getValueOfRootId()));
}
void fs::node(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())
cbk(dto::Responses::get_badreq_res("Unknown node"));
else if (inode->getValueOfIsFile() == 0) {
std::vector<uint64_t> children;
for (const db::INode& child : get_children(*inode)) children.push_back(child.getValueOfId());
cbk(dto::Responses::get_node_folder_res(
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) {
db::User user = dto::get_user(req);
auto inode = get_node_and_validate(user, node);
if (!inode.has_value())
cbk(dto::Responses::get_badreq_res("Unknown node"));
else
cbk(dto::Responses::get_path_res( generate_path(*inode)));
}
template<bool file>
void fs::create_node_req(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
Json::Value& json = *req->jsonObject();
try {
uint64_t parent = dto::json_get<uint64_t>(json, "parent").value();
std::string name = dto::json_get<std::string>(json, "name").value();
auto new_node = create_node(name, user, file, std::make_optional(parent));
if (std::holds_alternative<std::string>(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()));
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void fs::delete_node_req(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())
cbk(dto::Responses::get_badreq_res("Unknown node"));
else if (inode->getValueOfParentId() == 0)
cbk(dto::Responses::get_badreq_res("Can't delete root"));
else {
delete_node(*inode);
cbk(dto::Responses::get_success_res());
}
}
void fs::upload(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->getValueOfIsFile() == 0)
return cbk(dto::Responses::get_badreq_res("Can't upload to a directory"));
drogon::MultiPartParser mpp;
if (mpp.parse(req) != 0)
return cbk(dto::Responses::get_badreq_res("Failed to parse files"));
if (mpp.getFiles().size() != 1)
return cbk(dto::Responses::get_badreq_res("Exactly 1 file needed"));
const drogon::HttpFile& file = mpp.getFiles().at(0);
std::filesystem::path p("./files");
p /= std::to_string(inode->getValueOfId());
file.saveAs(p.string());
inode->setSize(file.fileLength());
db::MapperInode inode_mapper(drogon::app().getDbClient());
inode_mapper.update(*inode);
cbk(dto::Responses::get_success_res());
}
void fs::download(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
auto node_id = req->getOptionalParameter<uint64_t>("id");
if (!node_id.has_value()) {
cbk(dto::Responses::get_badreq_res("Invalid node"));
return;
}
auto inode = get_node_and_validate(user, *node_id);
if (!inode.has_value()) {
cbk(dto::Responses::get_badreq_res("Invalid node"));
return;
}
std::filesystem::path p("./files");
p /= std::to_string(inode->getValueOfId());
cbk(drogon::HttpResponse::newFileResponse(
p.string(),
inode->getValueOfName()
));
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,29 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "performance-unnecessary-value-param"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include "controllers.h"
#include "dto/dto.h"
namespace api {
void user::info(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
cbk(dto::Responses::get_user_info_res(
user.getValueOfName(),
user.getValueOfGitlab() != 0,
db::User_getEnumTfaType(user) != db::tfaTypes::NONE)
);
}
void user::delete_user(req_type req, cbk_type cbk) {
db::MapperUser user_mapper(drogon::app().getDbClient());
db::User user = dto::get_user(req);
auth::revoke_all(user);
fs::delete_node((fs::get_node(user.getValueOfRootId())).value(), true);
user_mapper.deleteOne(user);
cbk(dto::Responses::get_success_res());
}
}
#pragma clang diagnostic pop

11
backend/src/db/db.cpp Normal file
View File

@ -0,0 +1,11 @@
#include "db.h"
namespace db {
UserRole User_getEnumRole(const User& user) noexcept {
return (UserRole)user.getValueOfRole();
}
tfaTypes User_getEnumTfaType(const User& user) noexcept {
return (tfaTypes)user.getValueOfTfaType();
}
}

43
backend/src/db/db.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef BACKEND_DB_H
#define BACKEND_DB_H
#include <utility>
#include <drogon/utils/coroutine.h>
#include <drogon/drogon.h>
#include "model/Inode.h"
#include "model/Tokens.h"
#include "model/User.h"
const std::string jwt_secret = "CUM";
namespace db {
enum UserRole : int {
ADMIN = 2,
USER = 1,
DISABLED = 0
};
enum tfaTypes : int {
NONE = 0,
EMAIL = 1,
TOTP = 2
};
using INode = drogon_model::sqlite3::Inode;
using Token = drogon_model::sqlite3::Tokens;
using User = drogon_model::sqlite3::User;
using MapperInode = drogon::orm::Mapper<INode>;
using MapperToken = drogon::orm::Mapper<Token>;
using MapperUser = drogon::orm::Mapper<User>;
using Criteria = drogon::orm::Criteria;
using CompareOps = drogon::orm::CompareOperator;
UserRole User_getEnumRole(const User&) noexcept;
tfaTypes User_getEnumTfaType(const User&) noexcept;
}
#endif //BACKEND_DB_H

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,275 @@
/**
*
* Inode.h
* DO NOT EDIT. This file is generated by drogon_ctl
*
*/
#pragma once
#include <drogon/orm/Result.h>
#include <drogon/orm/Row.h>
#include <drogon/orm/Field.h>
#include <drogon/orm/SqlBinder.h>
#include <drogon/orm/Mapper.h>
#ifdef __cpp_impl_coroutine
#include <drogon/orm/CoroMapper.h>
#endif
#include <trantor/utils/Date.h>
#include <trantor/utils/Logger.h>
#include <json/json.h>
#include <string>
#include <memory>
#include <vector>
#include <tuple>
#include <stdint.h>
#include <iostream>
namespace drogon
{
namespace orm
{
class DbClient;
using DbClientPtr = std::shared_ptr<DbClient>;
}
}
namespace drogon_model
{
namespace sqlite3
{
class Inode
{
public:
struct Cols
{
static const std::string _id;
static const std::string _is_file;
static const std::string _name;
static const std::string _parent_id;
static const std::string _owner_id;
static const std::string _size;
};
const static int primaryKeyNumber;
const static std::string tableName;
const static bool hasPrimaryKey;
const static std::string primaryKeyName;
using PrimaryKeyType = uint64_t;
const PrimaryKeyType &getPrimaryKey() const;
/**
* @brief constructor
* @param r One row of records in the SQL query result.
* @param indexOffset Set the offset to -1 to access all columns by column names,
* otherwise access all columns by offsets.
* @note If the SQL is not a style of 'select * from table_name ...' (select all
* columns by an asterisk), please set the offset to -1.
*/
explicit Inode(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept;
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
*/
explicit Inode(const Json::Value &pJson) noexcept(false);
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
* @param pMasqueradingVector The aliases of table columns.
*/
Inode(const Json::Value &pJson, const std::vector<std::string> &pMasqueradingVector) noexcept(false);
Inode() = default;
void updateByJson(const Json::Value &pJson) noexcept(false);
void updateByMasqueradedJson(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector) noexcept(false);
static bool validateJsonForCreation(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForCreation(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForUpdate(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validJsonOfField(size_t index,
const std::string &fieldName,
const Json::Value &pJson,
std::string &err,
bool isForCreation);
/** For column id */
///Get the value of the column id, returns the default value if the column is null
const uint64_t &getValueOfId() 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> &getId() const noexcept;
///Set the value of the column id
void setId(const uint64_t &pId) noexcept;
/** For column is_file */
///Get the value of the column is_file, returns the default value if the column is null
const uint64_t &getValueOfIsFile() 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> &getIsFile() const noexcept;
///Set the value of the column is_file
void setIsFile(const uint64_t &pIsFile) noexcept;
/** For column name */
///Get the value of the column name, returns the default value if the column is null
const std::string &getValueOfName() 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<std::string> &getName() const noexcept;
///Set the value of the column name
void setName(const std::string &pName) noexcept;
void setName(std::string &&pName) noexcept;
void setNameToNull() noexcept;
/** For column parent_id */
///Get the value of the column parent_id, returns the default value if the column is null
const uint64_t &getValueOfParentId() 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> &getParentId() const noexcept;
///Set the value of the column parent_id
void setParentId(const uint64_t &pParentId) noexcept;
void setParentIdToNull() noexcept;
/** For column owner_id */
///Get the value of the column owner_id, returns the default value if the column is null
const uint64_t &getValueOfOwnerId() 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> &getOwnerId() const noexcept;
///Set the value of the column owner_id
void setOwnerId(const uint64_t &pOwnerId) noexcept;
/** For column size */
///Get the value of the column size, returns the default value if the column is null
const uint64_t &getValueOfSize() 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> &getSize() const noexcept;
///Set the value of the column size
void setSize(const uint64_t &pSize) noexcept;
void setSizeToNull() noexcept;
static size_t getColumnNumber() noexcept { return 6; }
static const std::string &getColumnName(size_t index) noexcept(false);
Json::Value toJson() const;
Json::Value toMasqueradedJson(const std::vector<std::string> &pMasqueradingVector) const;
/// Relationship interfaces
private:
friend drogon::orm::Mapper<Inode>;
#ifdef __cpp_impl_coroutine
friend drogon::orm::CoroMapper<Inode>;
#endif
static const std::vector<std::string> &insertColumns() noexcept;
void outputArgs(drogon::orm::internal::SqlBinder &binder) const;
const std::vector<std::string> updateColumns() const;
void updateArgs(drogon::orm::internal::SqlBinder &binder) const;
///For mysql or sqlite3
void updateId(const uint64_t id);
std::shared_ptr<uint64_t> id_;
std::shared_ptr<uint64_t> isFile_;
std::shared_ptr<std::string> name_;
std::shared_ptr<uint64_t> parentId_;
std::shared_ptr<uint64_t> ownerId_;
std::shared_ptr<uint64_t> size_;
struct MetaData
{
const std::string colName_;
const std::string colType_;
const std::string colDatabaseType_;
const ssize_t colLength_;
const bool isAutoVal_;
const bool isPrimaryKey_;
const bool notNull_;
};
static const std::vector<MetaData> metaData_;
bool dirtyFlag_[6]={ false };
public:
static const std::string &sqlForFindingByPrimaryKey()
{
static const std::string sql="select * from " + tableName + " where id = ?";
return sql;
}
static const std::string &sqlForDeletingByPrimaryKey()
{
static const std::string sql="delete from " + tableName + " where id = ?";
return sql;
}
std::string sqlForInserting(bool &needSelection) const
{
std::string sql="insert into " + tableName + " (";
size_t parametersCount = 0;
needSelection = false;
if(dirtyFlag_[1])
{
sql += "is_file,";
++parametersCount;
}
if(dirtyFlag_[2])
{
sql += "name,";
++parametersCount;
}
if(dirtyFlag_[3])
{
sql += "parent_id,";
++parametersCount;
}
if(dirtyFlag_[4])
{
sql += "owner_id,";
++parametersCount;
}
if(dirtyFlag_[5])
{
sql += "size,";
++parametersCount;
}
if(parametersCount > 0)
{
sql[sql.length()-1]=')';
sql += " values (";
}
else
sql += ") values (";
if(dirtyFlag_[1])
{
sql.append("?,");
}
if(dirtyFlag_[2])
{
sql.append("?,");
}
if(dirtyFlag_[3])
{
sql.append("?,");
}
if(dirtyFlag_[4])
{
sql.append("?,");
}
if(dirtyFlag_[5])
{
sql.append("?,");
}
if(parametersCount > 0)
{
sql.resize(sql.length() - 1);
}
sql.append(1, ')');
LOG_TRACE << sql;
return sql;
}
};
} // namespace sqlite3
} // namespace drogon_model

View File

@ -0,0 +1,631 @@
/**
*
* Tokens.cc
* DO NOT EDIT. This file is generated by drogon_ctl
*
*/
#include "Tokens.h"
#include <drogon/utils/Utilities.h>
#include <string>
using namespace drogon;
using namespace drogon::orm;
using namespace drogon_model::sqlite3;
const std::string Tokens::Cols::_id = "id";
const std::string Tokens::Cols::_owner_id = "owner_id";
const std::string Tokens::Cols::_exp = "exp";
const std::string Tokens::primaryKeyName = "id";
const bool Tokens::hasPrimaryKey = true;
const std::string Tokens::tableName = "tokens";
const std::vector<typename Tokens::MetaData> Tokens::metaData_={
{"id","uint64_t","integer",8,1,1,1},
{"owner_id","uint64_t","integer",8,0,0,1},
{"exp","uint64_t","integer",8,0,0,1}
};
const std::string &Tokens::getColumnName(size_t index) noexcept(false)
{
assert(index < metaData_.size());
return metaData_[index].colName_;
}
Tokens::Tokens(const Row &r, const ssize_t indexOffset) noexcept
{
if(indexOffset < 0)
{
if(!r["id"].isNull())
{
id_=std::make_shared<uint64_t>(r["id"].as<uint64_t>());
}
if(!r["owner_id"].isNull())
{
ownerId_=std::make_shared<uint64_t>(r["owner_id"].as<uint64_t>());
}
if(!r["exp"].isNull())
{
exp_=std::make_shared<uint64_t>(r["exp"].as<uint64_t>());
}
}
else
{
size_t offset = (size_t)indexOffset;
if(offset + 3 > r.size())
{
LOG_FATAL << "Invalid SQL result for this model";
return;
}
size_t index;
index = offset + 0;
if(!r[index].isNull())
{
id_=std::make_shared<uint64_t>(r[index].as<uint64_t>());
}
index = offset + 1;
if(!r[index].isNull())
{
ownerId_=std::make_shared<uint64_t>(r[index].as<uint64_t>());
}
index = offset + 2;
if(!r[index].isNull())
{
exp_=std::make_shared<uint64_t>(r[index].as<uint64_t>());
}
}
}
Tokens::Tokens(const Json::Value &pJson, const std::vector<std::string> &pMasqueradingVector) noexcept(false)
{
if(pMasqueradingVector.size() != 3)
{
LOG_ERROR << "Bad masquerading vector";
return;
}
if(!pMasqueradingVector[0].empty() && pJson.isMember(pMasqueradingVector[0]))
{
dirtyFlag_[0] = true;
if(!pJson[pMasqueradingVector[0]].isNull())
{
id_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[0]].asUInt64());
}
}
if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1]))
{
dirtyFlag_[1] = true;
if(!pJson[pMasqueradingVector[1]].isNull())
{
ownerId_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[1]].asUInt64());
}
}
if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2]))
{
dirtyFlag_[2] = true;
if(!pJson[pMasqueradingVector[2]].isNull())
{
exp_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[2]].asUInt64());
}
}
}
Tokens::Tokens(const Json::Value &pJson) noexcept(false)
{
if(pJson.isMember("id"))
{
dirtyFlag_[0]=true;
if(!pJson["id"].isNull())
{
id_=std::make_shared<uint64_t>((uint64_t)pJson["id"].asUInt64());
}
}
if(pJson.isMember("owner_id"))
{
dirtyFlag_[1]=true;
if(!pJson["owner_id"].isNull())
{
ownerId_=std::make_shared<uint64_t>((uint64_t)pJson["owner_id"].asUInt64());
}
}
if(pJson.isMember("exp"))
{
dirtyFlag_[2]=true;
if(!pJson["exp"].isNull())
{
exp_=std::make_shared<uint64_t>((uint64_t)pJson["exp"].asUInt64());
}
}
}
void Tokens::updateByMasqueradedJson(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector) noexcept(false)
{
if(pMasqueradingVector.size() != 3)
{
LOG_ERROR << "Bad masquerading vector";
return;
}
if(!pMasqueradingVector[0].empty() && pJson.isMember(pMasqueradingVector[0]))
{
if(!pJson[pMasqueradingVector[0]].isNull())
{
id_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[0]].asUInt64());
}
}
if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1]))
{
dirtyFlag_[1] = true;
if(!pJson[pMasqueradingVector[1]].isNull())
{
ownerId_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[1]].asUInt64());
}
}
if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2]))
{
dirtyFlag_[2] = true;
if(!pJson[pMasqueradingVector[2]].isNull())
{
exp_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[2]].asUInt64());
}
}
}
void Tokens::updateByJson(const Json::Value &pJson) noexcept(false)
{
if(pJson.isMember("id"))
{
if(!pJson["id"].isNull())
{
id_=std::make_shared<uint64_t>((uint64_t)pJson["id"].asUInt64());
}
}
if(pJson.isMember("owner_id"))
{
dirtyFlag_[1] = true;
if(!pJson["owner_id"].isNull())
{
ownerId_=std::make_shared<uint64_t>((uint64_t)pJson["owner_id"].asUInt64());
}
}
if(pJson.isMember("exp"))
{
dirtyFlag_[2] = true;
if(!pJson["exp"].isNull())
{
exp_=std::make_shared<uint64_t>((uint64_t)pJson["exp"].asUInt64());
}
}
}
const uint64_t &Tokens::getValueOfId() const noexcept
{
const static uint64_t defaultValue = uint64_t();
if(id_)
return *id_;
return defaultValue;
}
const std::shared_ptr<uint64_t> &Tokens::getId() const noexcept
{
return id_;
}
void Tokens::setId(const uint64_t &pId) noexcept
{
id_ = std::make_shared<uint64_t>(pId);
dirtyFlag_[0] = true;
}
const typename Tokens::PrimaryKeyType & Tokens::getPrimaryKey() const
{
assert(id_);
return *id_;
}
const uint64_t &Tokens::getValueOfOwnerId() const noexcept
{
const static uint64_t defaultValue = uint64_t();
if(ownerId_)
return *ownerId_;
return defaultValue;
}
const std::shared_ptr<uint64_t> &Tokens::getOwnerId() const noexcept
{
return ownerId_;
}
void Tokens::setOwnerId(const uint64_t &pOwnerId) noexcept
{
ownerId_ = std::make_shared<uint64_t>(pOwnerId);
dirtyFlag_[1] = true;
}
const uint64_t &Tokens::getValueOfExp() const noexcept
{
const static uint64_t defaultValue = uint64_t();
if(exp_)
return *exp_;
return defaultValue;
}
const std::shared_ptr<uint64_t> &Tokens::getExp() const noexcept
{
return exp_;
}
void Tokens::setExp(const uint64_t &pExp) noexcept
{
exp_ = std::make_shared<uint64_t>(pExp);
dirtyFlag_[2] = true;
}
void Tokens::updateId(const uint64_t id)
{
id_ = std::make_shared<uint64_t>(id);
}
const std::vector<std::string> &Tokens::insertColumns() noexcept
{
static const std::vector<std::string> inCols={
"owner_id",
"exp"
};
return inCols;
}
void Tokens::outputArgs(drogon::orm::internal::SqlBinder &binder) const
{
if(dirtyFlag_[1])
{
if(getOwnerId())
{
binder << getValueOfOwnerId();
}
else
{
binder << nullptr;
}
}
if(dirtyFlag_[2])
{
if(getExp())
{
binder << getValueOfExp();
}
else
{
binder << nullptr;
}
}
}
const std::vector<std::string> Tokens::updateColumns() const
{
std::vector<std::string> ret;
if(dirtyFlag_[1])
{
ret.push_back(getColumnName(1));
}
if(dirtyFlag_[2])
{
ret.push_back(getColumnName(2));
}
return ret;
}
void Tokens::updateArgs(drogon::orm::internal::SqlBinder &binder) const
{
if(dirtyFlag_[1])
{
if(getOwnerId())
{
binder << getValueOfOwnerId();
}
else
{
binder << nullptr;
}
}
if(dirtyFlag_[2])
{
if(getExp())
{
binder << getValueOfExp();
}
else
{
binder << nullptr;
}
}
}
Json::Value Tokens::toJson() const
{
Json::Value ret;
if(getId())
{
ret["id"]=(Json::UInt64)getValueOfId();
}
else
{
ret["id"]=Json::Value();
}
if(getOwnerId())
{
ret["owner_id"]=(Json::UInt64)getValueOfOwnerId();
}
else
{
ret["owner_id"]=Json::Value();
}
if(getExp())
{
ret["exp"]=(Json::UInt64)getValueOfExp();
}
else
{
ret["exp"]=Json::Value();
}
return ret;
}
Json::Value Tokens::toMasqueradedJson(
const std::vector<std::string> &pMasqueradingVector) const
{
Json::Value ret;
if(pMasqueradingVector.size() == 3)
{
if(!pMasqueradingVector[0].empty())
{
if(getId())
{
ret[pMasqueradingVector[0]]=(Json::UInt64)getValueOfId();
}
else
{
ret[pMasqueradingVector[0]]=Json::Value();
}
}
if(!pMasqueradingVector[1].empty())
{
if(getOwnerId())
{
ret[pMasqueradingVector[1]]=(Json::UInt64)getValueOfOwnerId();
}
else
{
ret[pMasqueradingVector[1]]=Json::Value();
}
}
if(!pMasqueradingVector[2].empty())
{
if(getExp())
{
ret[pMasqueradingVector[2]]=(Json::UInt64)getValueOfExp();
}
else
{
ret[pMasqueradingVector[2]]=Json::Value();
}
}
return ret;
}
LOG_ERROR << "Masquerade failed";
if(getId())
{
ret["id"]=(Json::UInt64)getValueOfId();
}
else
{
ret["id"]=Json::Value();
}
if(getOwnerId())
{
ret["owner_id"]=(Json::UInt64)getValueOfOwnerId();
}
else
{
ret["owner_id"]=Json::Value();
}
if(getExp())
{
ret["exp"]=(Json::UInt64)getValueOfExp();
}
else
{
ret["exp"]=Json::Value();
}
return ret;
}
bool Tokens::validateJsonForCreation(const Json::Value &pJson, std::string &err)
{
if(pJson.isMember("id"))
{
if(!validJsonOfField(0, "id", pJson["id"], err, true))
return false;
}
if(pJson.isMember("owner_id"))
{
if(!validJsonOfField(1, "owner_id", pJson["owner_id"], err, true))
return false;
}
else
{
err="The owner_id column cannot be null";
return false;
}
if(pJson.isMember("exp"))
{
if(!validJsonOfField(2, "exp", pJson["exp"], err, true))
return false;
}
else
{
err="The exp column cannot be null";
return false;
}
return true;
}
bool Tokens::validateMasqueradedJsonForCreation(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector,
std::string &err)
{
if(pMasqueradingVector.size() != 3)
{
err = "Bad masquerading vector";
return false;
}
try {
if(!pMasqueradingVector[0].empty())
{
if(pJson.isMember(pMasqueradingVector[0]))
{
if(!validJsonOfField(0, pMasqueradingVector[0], pJson[pMasqueradingVector[0]], err, true))
return false;
}
}
if(!pMasqueradingVector[1].empty())
{
if(pJson.isMember(pMasqueradingVector[1]))
{
if(!validJsonOfField(1, pMasqueradingVector[1], pJson[pMasqueradingVector[1]], err, true))
return false;
}
else
{
err="The " + pMasqueradingVector[1] + " column cannot be null";
return false;
}
}
if(!pMasqueradingVector[2].empty())
{
if(pJson.isMember(pMasqueradingVector[2]))
{
if(!validJsonOfField(2, pMasqueradingVector[2], pJson[pMasqueradingVector[2]], err, true))
return false;
}
else
{
err="The " + pMasqueradingVector[2] + " column cannot be null";
return false;
}
}
}
catch(const Json::LogicError &e)
{
err = e.what();
return false;
}
return true;
}
bool Tokens::validateJsonForUpdate(const Json::Value &pJson, std::string &err)
{
if(pJson.isMember("id"))
{
if(!validJsonOfField(0, "id", pJson["id"], err, false))
return false;
}
else
{
err = "The value of primary key must be set in the json object for update";
return false;
}
if(pJson.isMember("owner_id"))
{
if(!validJsonOfField(1, "owner_id", pJson["owner_id"], err, false))
return false;
}
if(pJson.isMember("exp"))
{
if(!validJsonOfField(2, "exp", pJson["exp"], err, false))
return false;
}
return true;
}
bool Tokens::validateMasqueradedJsonForUpdate(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector,
std::string &err)
{
if(pMasqueradingVector.size() != 3)
{
err = "Bad masquerading vector";
return false;
}
try {
if(!pMasqueradingVector[0].empty() && pJson.isMember(pMasqueradingVector[0]))
{
if(!validJsonOfField(0, pMasqueradingVector[0], pJson[pMasqueradingVector[0]], err, false))
return false;
}
else
{
err = "The value of primary key must be set in the json object for update";
return false;
}
if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1]))
{
if(!validJsonOfField(1, pMasqueradingVector[1], pJson[pMasqueradingVector[1]], err, false))
return false;
}
if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2]))
{
if(!validJsonOfField(2, pMasqueradingVector[2], pJson[pMasqueradingVector[2]], err, false))
return false;
}
}
catch(const Json::LogicError &e)
{
err = e.what();
return false;
}
return true;
}
bool Tokens::validJsonOfField(size_t index,
const std::string &fieldName,
const Json::Value &pJson,
std::string &err,
bool isForCreation)
{
switch(index)
{
case 0:
if(pJson.isNull())
{
err="The " + fieldName + " column cannot be null";
return false;
}
if(isForCreation)
{
err="The automatic primary key cannot be set";
return false;
}
if(!pJson.isUInt64())
{
err="Type error in the "+fieldName+" field";
return false;
}
break;
case 1:
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;
case 2:
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:
err="Internal error in the server";
return false;
break;
}
return true;
}

View File

@ -0,0 +1,211 @@
/**
*
* Tokens.h
* DO NOT EDIT. This file is generated by drogon_ctl
*
*/
#pragma once
#include <drogon/orm/Result.h>
#include <drogon/orm/Row.h>
#include <drogon/orm/Field.h>
#include <drogon/orm/SqlBinder.h>
#include <drogon/orm/Mapper.h>
#ifdef __cpp_impl_coroutine
#include <drogon/orm/CoroMapper.h>
#endif
#include <trantor/utils/Date.h>
#include <trantor/utils/Logger.h>
#include <json/json.h>
#include <string>
#include <memory>
#include <vector>
#include <tuple>
#include <stdint.h>
#include <iostream>
namespace drogon
{
namespace orm
{
class DbClient;
using DbClientPtr = std::shared_ptr<DbClient>;
}
}
namespace drogon_model
{
namespace sqlite3
{
class Tokens
{
public:
struct Cols
{
static const std::string _id;
static const std::string _owner_id;
static const std::string _exp;
};
const static int primaryKeyNumber;
const static std::string tableName;
const static bool hasPrimaryKey;
const static std::string primaryKeyName;
using PrimaryKeyType = uint64_t;
const PrimaryKeyType &getPrimaryKey() const;
/**
* @brief constructor
* @param r One row of records in the SQL query result.
* @param indexOffset Set the offset to -1 to access all columns by column names,
* otherwise access all columns by offsets.
* @note If the SQL is not a style of 'select * from table_name ...' (select all
* columns by an asterisk), please set the offset to -1.
*/
explicit Tokens(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept;
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
*/
explicit Tokens(const Json::Value &pJson) noexcept(false);
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
* @param pMasqueradingVector The aliases of table columns.
*/
Tokens(const Json::Value &pJson, const std::vector<std::string> &pMasqueradingVector) noexcept(false);
Tokens() = default;
void updateByJson(const Json::Value &pJson) noexcept(false);
void updateByMasqueradedJson(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector) noexcept(false);
static bool validateJsonForCreation(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForCreation(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForUpdate(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validJsonOfField(size_t index,
const std::string &fieldName,
const Json::Value &pJson,
std::string &err,
bool isForCreation);
/** For column id */
///Get the value of the column id, returns the default value if the column is null
const uint64_t &getValueOfId() 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> &getId() const noexcept;
///Set the value of the column id
void setId(const uint64_t &pId) noexcept;
/** For column owner_id */
///Get the value of the column owner_id, returns the default value if the column is null
const uint64_t &getValueOfOwnerId() 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> &getOwnerId() const noexcept;
///Set the value of the column owner_id
void setOwnerId(const uint64_t &pOwnerId) noexcept;
/** For column exp */
///Get the value of the column exp, returns the default value if the column is null
const uint64_t &getValueOfExp() 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> &getExp() const noexcept;
///Set the value of the column exp
void setExp(const uint64_t &pExp) noexcept;
static size_t getColumnNumber() noexcept { return 3; }
static const std::string &getColumnName(size_t index) noexcept(false);
Json::Value toJson() const;
Json::Value toMasqueradedJson(const std::vector<std::string> &pMasqueradingVector) const;
/// Relationship interfaces
private:
friend drogon::orm::Mapper<Tokens>;
#ifdef __cpp_impl_coroutine
friend drogon::orm::CoroMapper<Tokens>;
#endif
static const std::vector<std::string> &insertColumns() noexcept;
void outputArgs(drogon::orm::internal::SqlBinder &binder) const;
const std::vector<std::string> updateColumns() const;
void updateArgs(drogon::orm::internal::SqlBinder &binder) const;
///For mysql or sqlite3
void updateId(const uint64_t id);
std::shared_ptr<uint64_t> id_;
std::shared_ptr<uint64_t> ownerId_;
std::shared_ptr<uint64_t> exp_;
struct MetaData
{
const std::string colName_;
const std::string colType_;
const std::string colDatabaseType_;
const ssize_t colLength_;
const bool isAutoVal_;
const bool isPrimaryKey_;
const bool notNull_;
};
static const std::vector<MetaData> metaData_;
bool dirtyFlag_[3]={ false };
public:
static const std::string &sqlForFindingByPrimaryKey()
{
static const std::string sql="select * from " + tableName + " where id = ?";
return sql;
}
static const std::string &sqlForDeletingByPrimaryKey()
{
static const std::string sql="delete from " + tableName + " where id = ?";
return sql;
}
std::string sqlForInserting(bool &needSelection) const
{
std::string sql="insert into " + tableName + " (";
size_t parametersCount = 0;
needSelection = false;
if(dirtyFlag_[1])
{
sql += "owner_id,";
++parametersCount;
}
if(dirtyFlag_[2])
{
sql += "exp,";
++parametersCount;
}
if(parametersCount > 0)
{
sql[sql.length()-1]=')';
sql += " values (";
}
else
sql += ") values (";
if(dirtyFlag_[1])
{
sql.append("?,");
}
if(dirtyFlag_[2])
{
sql.append("?,");
}
if(parametersCount > 0)
{
sql.resize(sql.length() - 1);
}
sql.append(1, ')');
LOG_TRACE << sql;
return sql;
}
};
} // namespace sqlite3
} // namespace drogon_model

1762
backend/src/db/model/User.cc Normal file

File diff suppressed because it is too large Load Diff

361
backend/src/db/model/User.h Normal file
View File

@ -0,0 +1,361 @@
/**
*
* User.h
* DO NOT EDIT. This file is generated by drogon_ctl
*
*/
#pragma once
#include <drogon/orm/Result.h>
#include <drogon/orm/Row.h>
#include <drogon/orm/Field.h>
#include <drogon/orm/SqlBinder.h>
#include <drogon/orm/Mapper.h>
#ifdef __cpp_impl_coroutine
#include <drogon/orm/CoroMapper.h>
#endif
#include <trantor/utils/Date.h>
#include <trantor/utils/Logger.h>
#include <json/json.h>
#include <string>
#include <memory>
#include <vector>
#include <tuple>
#include <stdint.h>
#include <iostream>
namespace drogon
{
namespace orm
{
class DbClient;
using DbClientPtr = std::shared_ptr<DbClient>;
}
}
namespace drogon_model
{
namespace sqlite3
{
class User
{
public:
struct Cols
{
static const std::string _id;
static const std::string _gitlab;
static const std::string _name;
static const std::string _password;
static const std::string _role;
static const std::string _root_id;
static const std::string _tfa_type;
static const std::string _tfa_secret;
static const std::string _gitlab_at;
static const std::string _gitlab_rt;
};
const static int primaryKeyNumber;
const static std::string tableName;
const static bool hasPrimaryKey;
const static std::string primaryKeyName;
using PrimaryKeyType = uint64_t;
const PrimaryKeyType &getPrimaryKey() const;
/**
* @brief constructor
* @param r One row of records in the SQL query result.
* @param indexOffset Set the offset to -1 to access all columns by column names,
* otherwise access all columns by offsets.
* @note If the SQL is not a style of 'select * from table_name ...' (select all
* columns by an asterisk), please set the offset to -1.
*/
explicit User(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept;
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
*/
explicit User(const Json::Value &pJson) noexcept(false);
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
* @param pMasqueradingVector The aliases of table columns.
*/
User(const Json::Value &pJson, const std::vector<std::string> &pMasqueradingVector) noexcept(false);
User() = default;
void updateByJson(const Json::Value &pJson) noexcept(false);
void updateByMasqueradedJson(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector) noexcept(false);
static bool validateJsonForCreation(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForCreation(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForUpdate(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validJsonOfField(size_t index,
const std::string &fieldName,
const Json::Value &pJson,
std::string &err,
bool isForCreation);
/** For column id */
///Get the value of the column id, returns the default value if the column is null
const uint64_t &getValueOfId() 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> &getId() const noexcept;
///Set the value of the column id
void setId(const uint64_t &pId) noexcept;
/** For column gitlab */
///Get the value of the column gitlab, returns the default value if the column is null
const uint64_t &getValueOfGitlab() 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> &getGitlab() const noexcept;
///Set the value of the column gitlab
void setGitlab(const uint64_t &pGitlab) noexcept;
/** For column name */
///Get the value of the column name, returns the default value if the column is null
const std::string &getValueOfName() 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<std::string> &getName() const noexcept;
///Set the value of the column name
void setName(const std::string &pName) noexcept;
void setName(std::string &&pName) noexcept;
/** For column password */
///Get the value of the column password, returns the default value if the column is null
const std::string &getValueOfPassword() 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<std::string> &getPassword() const noexcept;
///Set the value of the column password
void setPassword(const std::string &pPassword) noexcept;
void setPassword(std::string &&pPassword) noexcept;
/** For column role */
///Get the value of the column role, returns the default value if the column is null
const uint64_t &getValueOfRole() 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> &getRole() const noexcept;
///Set the value of the column role
void setRole(const uint64_t &pRole) noexcept;
/** For column root_id */
///Get the value of the column root_id, returns the default value if the column is null
const uint64_t &getValueOfRootId() 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> &getRootId() const noexcept;
///Set the value of the column root_id
void setRootId(const uint64_t &pRootId) noexcept;
/** For column tfa_type */
///Get the value of the column tfa_type, returns the default value if the column is null
const uint64_t &getValueOfTfaType() 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> &getTfaType() const noexcept;
///Set the value of the column tfa_type
void setTfaType(const uint64_t &pTfaType) noexcept;
/** For column tfa_secret */
///Get the value of the column tfa_secret, returns the default value if the column is null
const std::vector<char> &getValueOfTfaSecret() const noexcept;
///Return the column value by std::string with binary data
std::string getValueOfTfaSecretAsString() 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<std::vector<char>> &getTfaSecret() const noexcept;
///Set the value of the column tfa_secret
void setTfaSecret(const std::vector<char> &pTfaSecret) noexcept;
void setTfaSecret(const std::string &pTfaSecret) noexcept;
void setTfaSecretToNull() noexcept;
/** For column gitlab_at */
///Get the value of the column gitlab_at, returns the default value if the column is null
const std::string &getValueOfGitlabAt() 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<std::string> &getGitlabAt() const noexcept;
///Set the value of the column gitlab_at
void setGitlabAt(const std::string &pGitlabAt) noexcept;
void setGitlabAt(std::string &&pGitlabAt) noexcept;
void setGitlabAtToNull() noexcept;
/** For column gitlab_rt */
///Get the value of the column gitlab_rt, returns the default value if the column is null
const std::string &getValueOfGitlabRt() 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<std::string> &getGitlabRt() const noexcept;
///Set the value of the column gitlab_rt
void setGitlabRt(const std::string &pGitlabRt) noexcept;
void setGitlabRt(std::string &&pGitlabRt) noexcept;
void setGitlabRtToNull() noexcept;
static size_t getColumnNumber() noexcept { return 10; }
static const std::string &getColumnName(size_t index) noexcept(false);
Json::Value toJson() const;
Json::Value toMasqueradedJson(const std::vector<std::string> &pMasqueradingVector) const;
/// Relationship interfaces
private:
friend drogon::orm::Mapper<User>;
#ifdef __cpp_impl_coroutine
friend drogon::orm::CoroMapper<User>;
#endif
static const std::vector<std::string> &insertColumns() noexcept;
void outputArgs(drogon::orm::internal::SqlBinder &binder) const;
const std::vector<std::string> updateColumns() const;
void updateArgs(drogon::orm::internal::SqlBinder &binder) const;
///For mysql or sqlite3
void updateId(const uint64_t id);
std::shared_ptr<uint64_t> id_;
std::shared_ptr<uint64_t> gitlab_;
std::shared_ptr<std::string> name_;
std::shared_ptr<std::string> password_;
std::shared_ptr<uint64_t> role_;
std::shared_ptr<uint64_t> rootId_;
std::shared_ptr<uint64_t> tfaType_;
std::shared_ptr<std::vector<char>> tfaSecret_;
std::shared_ptr<std::string> gitlabAt_;
std::shared_ptr<std::string> gitlabRt_;
struct MetaData
{
const std::string colName_;
const std::string colType_;
const std::string colDatabaseType_;
const ssize_t colLength_;
const bool isAutoVal_;
const bool isPrimaryKey_;
const bool notNull_;
};
static const std::vector<MetaData> metaData_;
bool dirtyFlag_[10]={ false };
public:
static const std::string &sqlForFindingByPrimaryKey()
{
static const std::string sql="select * from " + tableName + " where id = ?";
return sql;
}
static const std::string &sqlForDeletingByPrimaryKey()
{
static const std::string sql="delete from " + tableName + " where id = ?";
return sql;
}
std::string sqlForInserting(bool &needSelection) const
{
std::string sql="insert into " + tableName + " (";
size_t parametersCount = 0;
needSelection = false;
if(dirtyFlag_[1])
{
sql += "gitlab,";
++parametersCount;
}
if(dirtyFlag_[2])
{
sql += "name,";
++parametersCount;
}
if(dirtyFlag_[3])
{
sql += "password,";
++parametersCount;
}
if(dirtyFlag_[4])
{
sql += "role,";
++parametersCount;
}
if(dirtyFlag_[5])
{
sql += "root_id,";
++parametersCount;
}
if(dirtyFlag_[6])
{
sql += "tfa_type,";
++parametersCount;
}
if(dirtyFlag_[7])
{
sql += "tfa_secret,";
++parametersCount;
}
if(dirtyFlag_[8])
{
sql += "gitlab_at,";
++parametersCount;
}
if(dirtyFlag_[9])
{
sql += "gitlab_rt,";
++parametersCount;
}
if(parametersCount > 0)
{
sql[sql.length()-1]=')';
sql += " values (";
}
else
sql += ") values (";
if(dirtyFlag_[1])
{
sql.append("?,");
}
if(dirtyFlag_[2])
{
sql.append("?,");
}
if(dirtyFlag_[3])
{
sql.append("?,");
}
if(dirtyFlag_[4])
{
sql.append("?,");
}
if(dirtyFlag_[5])
{
sql.append("?,");
}
if(dirtyFlag_[6])
{
sql.append("?,");
}
if(dirtyFlag_[7])
{
sql.append("?,");
}
if(dirtyFlag_[8])
{
sql.append("?,");
}
if(dirtyFlag_[9])
{
sql.append("?,");
}
if(parametersCount > 0)
{
sql.resize(sql.length() - 1);
}
sql.append(1, ')');
LOG_TRACE << sql;
return sql;
}
};
} // namespace sqlite3
} // namespace drogon_model

View File

@ -0,0 +1,5 @@
{
"rdbms":"sqlite3",
"filename":"run/sqlite.db",
"tables":[]
}

56
backend/src/dto/dto.h Normal file
View File

@ -0,0 +1,56 @@
#ifndef BACKEND_DTO_H
#define BACKEND_DTO_H
#include <drogon/HttpResponse.h>
#include "db/db.h"
namespace dto {
template<typename T>
std::optional<T> json_get(const Json::Value& j, const std::string& key) {
return j.isMember(key)
? std::make_optional(j[key].as<T>())
: std::nullopt;
}
inline db::User get_user(const drogon::HttpRequestPtr& req) {
return req->attributes()->get<db::User>("user");
}
inline db::Token get_token(const drogon::HttpRequestPtr& req) {
return req->attributes()->get<db::Token>("token");
}
namespace Responses {
struct GetUsersEntry {
GetUsersEntry(int id, bool gitlab, bool tfa, std::string name, db::UserRole role)
: id(id), gitlab(gitlab), tfa(tfa), name(std::move(name)), role(role) {}
int id;
bool gitlab, tfa;
std::string name;
db::UserRole role;
};
drogon::HttpResponsePtr get_error_res(drogon::HttpStatusCode, const std::string &msg);
drogon::HttpResponsePtr get_success_res();
drogon::HttpResponsePtr get_success_res(Json::Value &);
inline drogon::HttpResponsePtr get_badreq_res(const std::string &msg) { return get_error_res(drogon::HttpStatusCode::k400BadRequest, msg); }
inline drogon::HttpResponsePtr get_unauth_res(const std::string &msg) { return get_error_res(drogon::HttpStatusCode::k401Unauthorized, msg); }
inline drogon::HttpResponsePtr get_forbdn_res(const std::string &msg) { return get_error_res(drogon::HttpStatusCode::k403Forbidden, msg); }
drogon::HttpResponsePtr get_login_res(const std::string &jwt);
drogon::HttpResponsePtr get_tfa_setup_res(const std::string& secret, const std::string& qrcode);
drogon::HttpResponsePtr get_user_info_res(const std::string& name, bool gitlab, bool tfa);
drogon::HttpResponsePtr get_admin_users_res(const std::vector<GetUsersEntry>& users);
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_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);
}
}
#endif //BACKEND_DTO_H

View File

@ -0,0 +1,98 @@
#include "dto.h"
namespace dto::Responses {
drogon::HttpResponsePtr get_error_res(drogon::HttpStatusCode code, const std::string& msg) {
Json::Value json;
json["statusCode"] = static_cast<int>(code);
json["message"] = msg;
auto res = drogon::HttpResponse::newHttpJsonResponse(json);
res->setStatusCode(code);
return res;
}
drogon::HttpResponsePtr get_success_res() {
Json::Value json;
return get_success_res(json);
}
drogon::HttpResponsePtr get_success_res(Json::Value& json) {
json["statusCode"] = 200;
auto res = drogon::HttpResponse::newHttpJsonResponse(json);
res->setStatusCode(drogon::HttpStatusCode::k200OK);
return res;
}
drogon::HttpResponsePtr get_login_res(const std::string &jwt) {
Json::Value json;
json["jwt"] = jwt;
return get_success_res(json);
}
drogon::HttpResponsePtr get_tfa_setup_res(const std::string& secret, const std::string& qrcode) {
Json::Value json;
json["secret"] = secret;
json["qrCode"] = qrcode;
return get_success_res(json);
}
drogon::HttpResponsePtr get_user_info_res(const std::string &name, bool gitlab, bool tfa) {
Json::Value json;
json["name"] = name;
json["gitlab"] = gitlab;
json["tfaEnabled"] = tfa;
return get_success_res(json);
}
drogon::HttpResponsePtr get_admin_users_res(const std::vector<GetUsersEntry>& users) {
Json::Value json;
for (const GetUsersEntry& user : users) {
Json::Value entry;
entry["id"] = user.id;
entry["gitlab"] = user.gitlab;
entry["name"] = user.name;
entry["role"] = user.role;
entry["tfaEnabled"] = user.tfa;
json["users"].append(entry);
}
return get_success_res(json);
}
drogon::HttpResponsePtr get_root_res(uint64_t root) {
Json::Value json;
json["rootId"] = root;
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 json;
json["id"] = id;
json["name"] = name;
json["isFile"] = false;
json["parent"] = (parent != nullptr) ? *parent : Json::Value::nullSingleton();
for (uint64_t child : children)
json["children"].append(child);
return get_success_res(json);
}
drogon::HttpResponsePtr get_node_file_res(uint64_t id, const std::string &name, const std::shared_ptr<uint64_t> &parent, uint64_t size) {
Json::Value json;
json["id"] = id;
json["name"] = name;
json["isFile"] = true;
json["parent"] = (parent != nullptr) ? *parent : Json::Value::nullSingleton();
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);
}
drogon::HttpResponsePtr get_new_node_res(uint64_t id) {
Json::Value json;
json["id"] = id;
return get_success_res(json);
}
}

View File

@ -0,0 +1,82 @@
#include "filters.h"
#include <drogon/utils/coroutine.h>
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include <jwt-cpp/jwt.h>
#include "db/db.h"
#include "dto/dto.h"
#include "controllers/controllers.h"
void cleanup_tokens(db::MapperToken& mapper) {
const uint64_t now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
mapper.deleteBy(
db::Criteria(db::Token::Cols::_exp, db::CompareOps::LE, now)
);
}
void Login::doFilter(const drogon::HttpRequestPtr& req, drogon::FilterCallback&& cb, drogon::FilterChainCallback&& ccb) {
std::string token_str;
if (req->path() == "/api/fs/download") {
token_str = req->getParameter("jwtToken");
} else {
std::string auth_header = req->getHeader("Authorization");
if (auth_header.empty() || (!auth_header.starts_with("Bearer ")))
return cb(dto::Responses::get_unauth_res("Unauthorized"));
token_str = auth_header.substr(7);
}
try {
auto token = jwt::decode<jwt::traits::kazuho_picojson>(token_str);
jwt::verify<jwt::traits::kazuho_picojson>()
.allow_algorithm(jwt::algorithm::hs256{jwt_secret})
.verify(token);
uint64_t token_id = token.get_payload_claim("jti").as_int();
uint64_t user_id = token.get_payload_claim("sub").as_int();
auto db = drogon::app().getDbClient();
db::MapperUser user_mapper(db);
db::MapperToken token_mapper(db);
cleanup_tokens(token_mapper);
db::Token db_token = token_mapper.findByPrimaryKey(token_id);
db::User db_user = user_mapper.findByPrimaryKey(db_token.getValueOfOwnerId());
if (db_user.getValueOfId() != user_id) throw std::exception();
if (db::User_getEnumRole(db_user) == db::UserRole::DISABLED) throw std::exception();
if (db_user.getValueOfGitlab() != 0) {
auto info = api::auth::get_gitlab_user(db_user.getValueOfGitlabAt());
if (!info.has_value()) {
auto tokens = api::auth::get_gitlab_tokens(req, db_user.getValueOfGitlabRt(), true);
info = api::auth::get_gitlab_user(tokens->at);
if (!tokens.has_value() || !info.has_value()) {
api::auth::revoke_all(db_user);
throw std::exception();
}
db_user.setGitlabAt(tokens->at);
db_user.setGitlabRt(tokens->rt);
user_mapper.update(db_user);
}
if (info->name != db_user.getValueOfName()) {
api::auth::revoke_all(db_user);
throw std::exception();
}
}
req->attributes()->insert("token", db_token);
req->attributes()->insert("user", db_user);
ccb();
} catch (const std::exception&) {
cb(dto::Responses::get_unauth_res("Unauthorized"));
}
}
void Admin::doFilter(const drogon::HttpRequestPtr& req, drogon::FilterCallback&& cb, drogon::FilterChainCallback&& ccb) {
db::User user = dto::get_user(req);
if (db::User_getEnumRole(user) != db::UserRole::ADMIN)
cb(dto::Responses::get_forbdn_res("Forbidden"));
else
ccb();
}

View File

@ -0,0 +1,14 @@
#ifndef BACKEND_FILTERS_H
#define BACKEND_FILTERS_H
#include <drogon/HttpFilter.h>
struct Login : public drogon::HttpFilter<Login> {
void doFilter(const drogon::HttpRequestPtr&, drogon::FilterCallback&&, drogon::FilterChainCallback&&) override;
};
struct Admin : public drogon::HttpFilter<Admin> {
void doFilter(const drogon::HttpRequestPtr&, drogon::FilterCallback&&, drogon::FilterChainCallback&&) override;
};
#endif //BACKEND_FILTERS_H

112
backend/src/main.cpp Normal file
View File

@ -0,0 +1,112 @@
#include <filesystem>
#include <drogon/drogon.h>
#include <curl/curl.h>
#include "dto/dto.h"
void cleanup() {
std::cout << "Stopping..." << std::endl;
drogon::app().quit();
std::cout << "Cleanup up uploads...";
std::filesystem::remove_all("uploads");
std::cout << " [Done]" << std::endl;
std::cout << "Goodbye!" << std::endl;
}
int main() {
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")) {
std::cout << "Creating files..." << std::flush;
std::filesystem::create_directory("files");
std::cout << " [Done]" << std::endl;
}
if (!std::filesystem::exists("logs")) {
std::cout << "Creating logs..." << std::flush;
std::filesystem::create_directory("logs");
std::cout << " [Done]" << std::endl;
}
auto* loop = drogon::app().getLoop();
loop->queueInLoop([]{
std::cout << "Starting..." << std::endl;
std::cout << "Creating db tables..." << std::flush;
auto db = drogon::app().getDbClient();
db->execSqlSync("CREATE TABLE IF NOT EXISTS 'tokens' (\n"
" 'id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n"
" 'owner_id' INTEGER NOT NULL,\n"
" 'exp' INTEGER NOT NULL\n"
")");
db->execSqlSync("CREATE TABLE IF NOT EXISTS 'user' (\n"
" 'id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n"
" 'gitlab' INTEGER NOT NULL,\n"
" 'name' TEXT NOT NULL,\n"
" 'password' TEXT NOT NULL,\n"
" 'role' INTEGER NOT NULL,\n"
" 'root_id' INTEGER NOT NULL,\n"
" 'tfa_type' INTEGER NOT NULL,\n"
" 'tfa_secret' BLOB,\n"
" 'gitlab_at' TEXT,\n"
" 'gitlab_rt' TEXT\n"
")");
db->execSqlSync("CREATE TABLE IF NOT EXISTS 'inode' (\n"
" 'id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n"
" 'is_file' INTEGER NOT NULL,\n"
" 'name' TEXT,\n"
" 'parent_id' INTEGER,\n"
" 'owner_id' INTEGER NOT NULL,\n"
" 'size' INTEGER\n"
")");
std::cout << " [Done]" << std::endl;
std::cout << "Started!" << std::endl;
std::cout << "Registered paths: " << std::endl;
auto handlers = drogon::app().getHandlersInfo();
for (const auto& handler : handlers) {
std::cout << " ";
if (std::get<1>(handler) == drogon::HttpMethod::Post) std::cout << "POST ";
else std::cout << "GET ";
std::string func = std::get<2>(handler).substr(16);
func.resize(30, ' ');
std::cout << '[' << func << "] ";
std::cout << std::get<0>(handler) << std::endl;
}
std::cout << "Listening on:" << std::endl;
auto listeners = drogon::app().getListeners();
for (const auto& listener : listeners) {
std::cout << " " << listener.toIpPort() << std::endl;
}
});
Json::Value access_logger;
access_logger["name"] = "drogon::plugin::AccessLogger";
Json::Value config;
config["plugins"].append(access_logger);
drogon::app()
.setClientMaxBodySize(1024L * 1024L * 1024L * 1024L) // 1 TB
.loadConfigJson(config)
.createDbClient("sqlite3", "", 0, "", "", "", 1, "sqlite.db")
.setCustom404Page(drogon::HttpResponse::newFileResponse("./static/index.html"), false)
.setDocumentRoot("./static")
.setBrStatic(true)
.setStaticFilesCacheTime(0)
.setLogPath("./logs")
.setLogLevel(trantor::Logger::LogLevel::kDebug)
.setIntSignalHandler(cleanup)
.setTermSignalHandler(cleanup)
.addListener("0.0.0.0", 1234)
.setThreadNum(2);
std::cout << "Setup done!" << std::endl;
drogon::app().run();
}

17
backend/vcpkg.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
"name": "backend",
"version-string": "1.0.0",
"dependencies": [
{
"name": "drogon",
"features": ["orm", "sqlite3"]
},
"jwt-cpp",
"botan",
"curl",
"pngpp",
"nayuki-qr-code-generator",
"libpng"
]
}

View File

@ -1,8 +0,0 @@
export * as Requests from './requests';
export * as Responses from './responses';
export {
UserRole,
validateSync,
validateAsync,
validateAsyncInline
} from './utils';

View File

@ -1,17 +0,0 @@
import { BaseRequest } from './base';
import { IsEnum, IsNumber } from 'class-validator';
import { UserRole } from '../utils';
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,20 +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 DeleteRequest extends BaseRequest {
@IsInt()
@Min(1)
node: number;
}
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,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,41 +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<T extends { new (...args: any[]): any }>(
constr: T
) {
return class extends constr {
constructor(...args: any[]) {
super(...args);
validateSync(this);
}
};
}

View File

@ -1,3 +1,3 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset']
presets: ["@vue/cli-plugin-babel/preset"],
};

View File

@ -9,6 +9,8 @@
},
"dependencies": {
"axios": "^0.27.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"core-js": "^3.8.3",
"filesize": "^9.0.11",
"jwt-decode": "^3.1.2",
@ -27,8 +29,6 @@
"@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/eslint-config-typescript": "^9.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",

View File

@ -1,62 +1,62 @@
<script setup async lang="ts">
import { provide, ref } from 'vue';
import { useRouter } from 'vue-router';
import { TokenInjectType } from '@/api';
import { provide, ref } from "vue";
import { useRouter } from "vue-router";
import { TokenInjectType } from "@/api";
const router = useRouter();
const jwt = ref<string | null>(localStorage.getItem('token'));
const jwt = ref<string | null>(localStorage.getItem("token"));
function setToken(token: string) {
jwt.value = token;
localStorage.setItem('token', token);
jwt.value = token;
localStorage.setItem("token", token);
}
function logout() {
jwt.value = null;
localStorage.removeItem('token');
router.push({ name: 'login' });
jwt.value = null;
localStorage.removeItem("token");
router.push({ name: "login" });
}
provide<TokenInjectType>('jwt', {
jwt,
setToken,
logout
provide<TokenInjectType>("jwt", {
jwt,
setToken,
logout,
});
</script>
<template>
<nav>
<template v-if="jwt != null">
<router-link to="/">Files</router-link>
<span style="margin-left: 2em" />
<router-link to="/profile">Profile</router-link>
<span style="margin-left: 2em" />
<router-link to="/login" @click="logout()">Logout</router-link>
</template>
</nav>
<router-view />
<nav>
<template v-if="jwt != null">
<router-link to="/">Files</router-link>
<span style="margin-left: 2em" />
<router-link to="/profile">Profile</router-link>
<span style="margin-left: 2em" />
<router-link to="/login" @click="logout()">Logout</router-link>
</template>
</nav>
<router-view />
</template>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav {
padding: 30px;
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>

View File

@ -1,12 +1,12 @@
<script setup lang="ts">
import App from './App';
import App from "./App";
</script>
<template>
<Suspense>
<App></App>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
<Suspense>
<App></App>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>

View File

@ -1,54 +1,54 @@
import { Requests, Responses, UserRole, get_token, post_token } from './base';
import { Requests, Responses, UserRole, get_token, post_token } from "./base";
export const get_users = (token: string): Promise<Responses.Admin.GetUsers> =>
get_token('/api/admin/users', token);
get_token("/api/admin/users", token);
export const set_role = (
user: number,
role: UserRole,
token: string
user: number,
role: UserRole,
token: string
): Promise<Responses.Admin.SetUserRole | Responses.ErrorResponse> =>
post_token<Requests.Admin.SetUserRole>(
'/api/admin/set_role',
{
user,
role
},
token
);
post_token<Requests.Admin.SetUserRole>(
"/api/admin/set_role",
{
user,
role,
},
token
);
export const logout = (
user: number,
token: string
user: number,
token: string
): Promise<Responses.Admin.LogoutAllUser | Responses.ErrorResponse> =>
post_token<Requests.Admin.LogoutAll>(
'/api/admin/logout',
{
user
},
token
);
post_token<Requests.Admin.LogoutAll>(
"/api/admin/logout",
{
user,
},
token
);
export const delete_user = (
user: number,
token: string
user: number,
token: string
): Promise<Responses.Admin.DeleteUser | Responses.ErrorResponse> =>
post_token<Requests.Admin.DeleteUser>(
'/api/admin/delete',
{
user
},
token
);
post_token<Requests.Admin.DeleteUser>(
"/api/admin/delete",
{
user,
},
token
);
export const disable_tfa = (
user: number,
token: string
user: number,
token: string
): Promise<Responses.Admin.DisableTfa | Responses.ErrorResponse> =>
post_token<Requests.Admin.DisableTfa>(
'/api/admin/disable_2fa',
{
user
},
token
);
post_token<Requests.Admin.DisableTfa>(
"/api/admin/disable_2fa",
{
user,
},
token
);

View File

@ -1,93 +1,93 @@
import { Responses, Requests, post, post_token } from './base';
import { Responses, Requests, post, post_token } from "./base";
export const auth_login = (
username: string,
password: string,
otp?: string
username: string,
password: string,
otp?: string
): Promise<
| Responses.Auth.LoginResponse
| Responses.Auth.TfaRequiredResponse
| Responses.ErrorResponse
| Responses.Auth.LoginResponse
| Responses.Auth.TfaRequiredResponse
| Responses.ErrorResponse
> =>
post<Requests.Auth.LoginRequest>('/api/auth/login', {
username: username,
password: password,
otp: otp
});
post<Requests.Auth.LoginRequest>("/api/auth/login", {
username: username,
password: password,
otp: otp,
});
export const auth_signup = (
username: string,
password: string
username: string,
password: string
): Promise<Responses.Auth.SignupResponse | Responses.ErrorResponse> =>
post<Requests.Auth.SignUpRequest>('/api/auth/signup', {
username: username,
password: password
});
post<Requests.Auth.SignUpRequest>("/api/auth/signup", {
username: username,
password: password,
});
export const refresh_token = (
token: string
token: string
): Promise<Responses.Auth.RefreshResponse | Responses.ErrorResponse> =>
post_token('/api/auth/refresh', {}, token);
post_token("/api/auth/refresh", {}, token);
export const change_password = (
oldPw: string,
newPw: string,
token: string
oldPw: string,
newPw: string,
token: string
): Promise<Responses.Auth.ChangePasswordResponse | Responses.ErrorResponse> =>
post_token<Requests.Auth.ChangePasswordRequest>(
'/api/auth/change_password',
{
oldPassword: oldPw,
newPassword: newPw
},
token
);
post_token<Requests.Auth.ChangePasswordRequest>(
"/api/auth/change_password",
{
oldPassword: oldPw,
newPassword: newPw,
},
token
);
export const logout_all = (
token: string
token: string
): Promise<Responses.Auth.LogoutAllResponse | Responses.ErrorResponse> =>
post_token('/api/auth/logout_all', {}, token);
post_token("/api/auth/logout_all", {}, token);
export function tfa_setup(
mail: false,
token: string
mail: false,
token: string
): Promise<Responses.Auth.RequestTotpTfaResponse | Responses.ErrorResponse>;
export function tfa_setup(
mail: true,
token: string
mail: true,
token: string
): Promise<Responses.Auth.RequestEmailTfaResponse | Responses.ErrorResponse>;
export function tfa_setup(
mail: boolean,
token: string
mail: boolean,
token: string
): Promise<
| Responses.Auth.RequestEmailTfaResponse
| Responses.Auth.RequestTotpTfaResponse
| Responses.ErrorResponse
| Responses.Auth.RequestEmailTfaResponse
| Responses.Auth.RequestTotpTfaResponse
| Responses.ErrorResponse
> {
return post_token<Requests.Auth.TfaSetup>(
'/api/auth/2fa/setup',
{
mail
},
token
);
return post_token<Requests.Auth.TfaSetup>(
"/api/auth/2fa/setup",
{
mail,
},
token
);
}
export const tfa_complete = (
mail: boolean,
code: string,
token: string
mail: boolean,
code: string,
token: string
): Promise<Responses.Auth.TfaCompletedResponse | Responses.ErrorResponse> =>
post_token<Requests.Auth.TfaComplete>(
'/api/auth/2fa/complete',
{
mail,
code
},
token
);
post_token<Requests.Auth.TfaComplete>(
"/api/auth/2fa/complete",
{
mail,
code,
},
token
);
export const tfa_disable = (
token: string
token: string
): Promise<Responses.Auth.RemoveTfaResponse | Responses.ErrorResponse> =>
post_token('/api/auth/2fa/disable', {}, token);
post_token("/api/auth/2fa/disable", {}, token);

View File

@ -1,62 +1,62 @@
import axios from 'axios';
import { Requests, Responses, UserRole } from '../../../dto';
import axios from "axios";
import { Requests, Responses, UserRole } from "../dto";
export { Requests, Responses, UserRole };
export const post = <T extends Requests.BaseRequest>(url: string, data: T) =>
axios
.post(url, data, {
headers: { 'Content-type': 'application/json' }
})
.then((res) => res.data)
.catch((err) => err.response.data);
axios
.post(url, data, {
headers: { "Content-type": "application/json" },
})
.then((res) => res.data)
.catch((err) => err.response.data);
export const post_token = <T extends Requests.BaseRequest>(
url: string,
data: T,
token: string
url: string,
data: T,
token: string
) =>
axios
.post(url, data, {
headers: {
Authorization: 'Bearer ' + token,
'Content-type': 'application/json'
}
})
.then((res) => res.data)
.catch((err) => err.response.data);
axios
.post(url, data, {
headers: {
Authorization: "Bearer " + token,
"Content-type": "application/json",
},
})
.then((res) => res.data)
.catch((err) => err.response.data);
export const post_token_form = (
url: string,
data: FormData,
token: string,
onProgress: (progressEvent: ProgressEvent) => void
url: string,
data: FormData,
token: string,
onProgress: (progressEvent: ProgressEvent) => void
) =>
axios
.post(url, data, {
headers: {
Authorization: 'Bearer ' + token,
'Content-type': 'multipart/form-data'
},
onUploadProgress: onProgress
})
.then((res) => res.data)
.catch((err) => err.response.data);
axios
.post(url, data, {
headers: {
Authorization: "Bearer " + token,
"Content-type": "multipart/form-data",
},
onUploadProgress: onProgress,
})
.then((res) => res.data)
.catch((err) => err.response.data);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const get = (url: string) =>
axios
.get(url)
.then((res) => res.data)
.catch((err) => err.response.data);
axios
.get(url)
.then((res) => res.data)
.catch((err) => err.response.data);
export const get_token = (url: string, token: string) =>
axios
.get(url, {
headers: { Authorization: 'Bearer ' + token }
})
.then((res) => res.data)
.catch((err) => err.response.data);
axios
.get(url, {
headers: { Authorization: "Bearer " + token },
})
.then((res) => res.data)
.catch((err) => err.response.data);
export const isErrorResponse = (
res: Responses.BaseResponse
res: Responses.BaseResponse
): res is Responses.ErrorResponse => res.statusCode != 200;

View File

@ -1,95 +1,84 @@
import {
Responses,
Requests,
get_token,
post_token,
post_token_form,
isErrorResponse
} from './base';
Responses,
Requests,
get_token,
post_token,
post_token_form,
isErrorResponse,
} from "./base";
export const get_root = (
token: string
token: string
): Promise<Responses.FS.GetRootResponse | Responses.ErrorResponse> =>
get_token('/api/fs/root', token);
get_token("/api/fs/root", token);
export const get_node = (
token: string,
node: number
token: string,
node: number
): Promise<Responses.FS.GetNodeResponse | Responses.ErrorResponse> =>
get_token(`/api/fs/node/${node}`, token);
get_token(`/api/fs/node/${node}`, token);
export const get_path = (
token: string,
node: number
token: string,
node: number
): Promise<Responses.FS.GetPathResponse | Responses.ErrorResponse> =>
get_token(`/api/fs/path/${node}`, token);
get_token(`/api/fs/path/${node}`, token);
export const create_folder = (
token: string,
parent: number,
name: string
token: string,
parent: number,
name: string
): Promise<Responses.FS.CreateFolderResponse | Responses.ErrorResponse> =>
post_token<Requests.FS.CreateFolderRequest>(
'/api/fs/createFolder',
{
parent: parent,
name: name
},
token
);
post_token<Requests.FS.CreateFolderRequest>(
"/api/fs/createFolder",
{
parent: parent,
name: name,
},
token
);
export const create_file = (
token: string,
parent: number,
name: string
token: string,
parent: number,
name: string
): Promise<Responses.FS.CreateFileResponse | Responses.ErrorResponse> =>
post_token<Requests.FS.CreateFileRequest>(
'/api/fs/createFile',
{
parent: parent,
name: name
},
token
);
post_token<Requests.FS.CreateFileRequest>(
"/api/fs/createFile",
{
parent: parent,
name: name,
},
token
);
export const delete_node = (
token: string,
node: number
token: string,
node: number
): Promise<Responses.FS.DeleteResponse | Responses.ErrorResponse> =>
post_token<Requests.FS.DeleteRequest>(
'/api/fs/delete',
{
node: node
},
token
);
post_token(`/api/fs/delete/${node}`, {}, token);
export const upload_file = async (
token: string,
parent: number,
file: File,
onProgress: (progressEvent: ProgressEvent) => void
token: string,
parent: number,
file: File,
onProgress: (progressEvent: ProgressEvent) => void
): Promise<Responses.FS.UploadFileResponse | Responses.ErrorResponse> => {
const node = await create_file(token, parent, file.name);
if (isErrorResponse(node)) return node;
const node = await create_file(token, parent, file.name);
if (isErrorResponse(node)) return node;
const form = new FormData();
form.set('file', file);
return post_token_form(
`/api/fs/upload/${node.id}`,
form,
token,
onProgress
);
const form = new FormData();
form.set("file", file);
return post_token_form(`/api/fs/upload/${node.id}`, form, token, onProgress);
};
export function download_file(token: string, id: number) {
const form = document.createElement('form');
form.method = 'post';
form.target = '_blank';
form.action = '/api/fs/download';
form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${id}">`;
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
const form = document.createElement("form");
form.method = "post";
form.target = "_blank";
form.action = "/api/fs/download";
form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${id}">`;
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
}

View File

@ -1,6 +1,6 @@
export { Requests, Responses, UserRole, isErrorResponse } from './base';
export * as Auth from './auth';
export * as FS from './fs';
export * as User from './user';
export * as Admin from './admin';
export * from './util';
export { Requests, Responses, UserRole, isErrorResponse } from "./base";
export * as Auth from "./auth";
export * as FS from "./fs";
export * as User from "./user";
export * as Admin from "./admin";
export * from "./util";

View File

@ -1,11 +1,11 @@
import { Responses, get_token, post_token } from '@/api/base';
import { Responses, get_token, post_token } from "@/api/base";
export const get_user_info = (
token: string
token: string
): Promise<Responses.User.UserInfoResponse | Responses.ErrorResponse> =>
get_token('/api/user/info', token);
get_token("/api/user/info", token);
export const delete_user = (
token: string
token: string
): Promise<Responses.User.DeleteUserResponse | Responses.ErrorResponse> =>
post_token('/api/user/delete', {}, token);
post_token("/api/user/delete", {}, token);

View File

@ -1,25 +1,25 @@
import jwtDecode, { JwtPayload } from 'jwt-decode';
import { Ref, UnwrapRef } from 'vue';
import { isErrorResponse } from './base';
import { refresh_token } from './auth';
import jwtDecode, { JwtPayload } from "jwt-decode";
import { Ref, UnwrapRef } from "vue";
import { isErrorResponse } from "./base";
import { refresh_token } from "./auth";
export async function check_token(
token: TokenInjectType
token: TokenInjectType
): Promise<string | void> {
if (!token.jwt.value) return token.logout();
const payload = jwtDecode<JwtPayload>(token.jwt.value);
if (!payload) return token.logout();
// Expires in more than 60 Minute
if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60))
return token.jwt.value;
const new_token = await refresh_token(token.jwt.value);
if (isErrorResponse(new_token)) return token.logout();
token.setToken(new_token.jwt);
return new_token.jwt;
if (!token.jwt.value) return token.logout();
const payload = jwtDecode<JwtPayload>(token.jwt.value);
if (!payload) return token.logout();
// Expires in more than 60 Minute
if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60))
return token.jwt.value;
const new_token = await refresh_token(token.jwt.value);
if (isErrorResponse(new_token)) return token.logout();
token.setToken(new_token.jwt);
return new_token.jwt;
}
export type TokenInjectType = {
jwt: Ref<UnwrapRef<string | null>>;
setToken: (token: string) => void;
logout: () => void;
jwt: Ref<UnwrapRef<string | null>>;
setToken: (token: string) => void;
logout: () => void;
};

View File

@ -1,40 +1,40 @@
<script setup lang="ts">
import { defineEmits, defineProps, inject } from 'vue';
import { check_token, FS, Responses, TokenInjectType } from '@/api';
import { defineEmits, defineProps, inject } from "vue";
import { check_token, FS, Responses, TokenInjectType } from "@/api";
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const props = defineProps<{
node: Responses.FS.GetNodeResponse;
node: Responses.FS.GetNodeResponse;
}>();
const emit = defineEmits<{
(e: 'reloadNode'): void;
(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');
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);
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>
<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,108 +1,101 @@
<script setup lang="ts">
import { defineEmits, defineProps, inject, reactive, ref, watch } from 'vue';
import { FS, Responses, check_token, TokenInjectType } from '@/api';
import DirEntry from '@/components/FSView/DirEntry.vue';
import UploadFileDialog from '@/components/UploadDialog/UploadFileDialog.vue';
import { NModal } from 'naive-ui';
import { defineEmits, defineProps, inject, reactive, ref, watch } from "vue";
import { FS, Responses, check_token, TokenInjectType } 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;
node: Responses.FS.GetNodeResponse;
}>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const emit = defineEmits<{
(e: 'reloadNode'): void;
(e: 'gotoRoot'): void;
(e: "reloadNode"): void;
(e: "gotoRoot"): void;
}>();
const fileInput = ref<HTMLInputElement>();
const uploadDialog = ref();
const uploadDialogShow = ref(false);
const new_folder_name = ref('');
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: '..'
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 }
() => 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');
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;
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');
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>
<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,38 +1,38 @@
<script setup lang="ts">
import { defineProps, inject } from 'vue';
import { check_token, FS, Responses, TokenInjectType } from '@/api';
import { defineProps, inject } from "vue";
import { check_token, FS, Responses, TokenInjectType } from "@/api";
const props = defineProps<{
node: Responses.FS.GetNodeResponse;
node: Responses.FS.GetNodeResponse;
}>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
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);
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);
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>
<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

@ -1,156 +1,140 @@
<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>
<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';
import { defineComponent } from "vue";
export default defineComponent({
name: 'HelloWorld',
props: {
msg: String
}
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;
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
color: #42b983;
}
</style>

View File

@ -1,51 +1,51 @@
<script setup lang="ts">
import { defineProps, defineExpose, ref } from 'vue';
import { isErrorResponse, FS } from '@/api';
import { NProgress } from 'naive-ui';
import filesize from 'filesize';
import { defineProps, defineExpose, ref } from "vue";
import { isErrorResponse, FS } from "@/api";
import { NProgress } from "naive-ui";
import filesize from "filesize";
const props = defineProps<{
file: File;
file: File;
}>();
const progress = ref(0);
const percentage = ref(0);
const err = ref('');
const status = ref('info');
const err = ref("");
const status = ref("info");
async function startUpload(parent: number, token: string) {
const resp = await FS.upload_file(token, parent, props.file, (e) => {
progress.value = e.loaded;
percentage.value = (e.loaded / e.total) * 100;
});
percentage.value = 100;
if (isErrorResponse(resp)) {
err.value = resp.message ?? 'Error';
status.value = 'error';
} else status.value = 'success';
const resp = await FS.upload_file(token, parent, props.file, (e) => {
progress.value = e.loaded;
percentage.value = (e.loaded / e.total) * 100;
});
percentage.value = 100;
if (isErrorResponse(resp)) {
err.value = resp.message ?? "Error";
status.value = "error";
} else status.value = "success";
}
defineExpose({
startUpload
startUpload,
});
</script>
<template>
<div v-if="percentage < 100">
{{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} -
{{ Math.floor(percentage * 1000) / 1000 }}%
</div>
<div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div>
<div v-else>{{ file.name }} - Completed</div>
<n-progress
type="line"
:percentage="percentage"
:height="20"
:status="status"
border-radius="10px 0"
fill-border-radius="10px 0"
:show-indicator="false"
/>
<div v-if="percentage < 100">
{{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} -
{{ Math.floor(percentage * 1000) / 1000 }}%
</div>
<div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div>
<div v-else>{{ file.name }} - Completed</div>
<n-progress
type="line"
:percentage="percentage"
:height="20"
:status="status"
border-radius="10px 0"
fill-border-radius="10px 0"
:show-indicator="false"
/>
</template>
<style scoped></style>

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import { defineProps, defineExpose, ref, inject } from 'vue';
import { check_token, TokenInjectType } from '@/api';
import UploadEntry from '@/components/UploadDialog/UploadEntry.vue';
import { NCard } from 'naive-ui';
import { defineProps, defineExpose, ref, inject } from "vue";
import { check_token, TokenInjectType } from "@/api";
import UploadEntry from "@/components/UploadDialog/UploadEntry.vue";
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 done = ref(false);
@ -12,37 +12,32 @@ let canCloseResolve = null;
const canClose = new Promise((r) => (canCloseResolve = r));
async function startUpload(parent: number) {
const token = await check_token(jwt);
if (!token) return;
await Promise.all(
entries.value.map((entry) => entry.startUpload(parent, token))
);
done.value = true;
await canClose;
const token = await check_token(jwt);
if (!token) return;
await Promise.all(
entries.value.map((entry) => entry.startUpload(parent, token))
);
done.value = true;
await canClose;
}
defineExpose({
startUpload
startUpload,
});
defineProps<{
files: File[];
files: File[];
}>();
</script>
<template>
<n-card title="Upload Files">
<div>
<UploadEntry
v-for="f in files"
:key="f.name"
ref="entries"
:file="f"
/>
</div>
<div>
<button v-if="done" @click="canCloseResolve()">Close</button>
</div>
</n-card>
<n-card title="Upload Files">
<div>
<UploadEntry v-for="f in files" :key="f.name" ref="entries" :file="f" />
</div>
<div>
<button v-if="done" @click="canCloseResolve()">Close</button>
</div>
</n-card>
</template>
<style scoped></style>

View File

@ -0,0 +1,8 @@
export * as Requests from "./requests";
export * as Responses from "./responses";
export {
UserRole,
validateSync,
validateAsync,
validateAsyncInline,
} from "./utils";

View File

@ -0,0 +1,17 @@
import { BaseRequest } from "./base";
import { IsEnum, IsNumber } from "class-validator";
import { UserRole } from "../utils";
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

@ -0,0 +1,50 @@
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

@ -0,0 +1,14 @@
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

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

View File

@ -0,0 +1,61 @@
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,33 +1,33 @@
import { SuccessResponse } from './base';
import { IsBase32, IsJWT, IsNotEmpty } from 'class-validator';
import { ValidateConstructor } from '../utils';
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;
}
constructor(jwt: string) {
super();
this.jwt = jwt;
}
@IsNotEmpty()
@IsJWT()
jwt: string;
@IsNotEmpty()
@IsJWT()
jwt: string;
}
@ValidateConstructor
export class RequestTotpTfaResponse extends SuccessResponse {
constructor(qrCode: string, secret: string) {
super();
this.qrCode = qrCode;
this.secret = secret;
}
constructor(qrCode: string, secret: string) {
super();
this.qrCode = qrCode;
this.secret = secret;
}
@IsNotEmpty()
qrCode: string;
@IsNotEmpty()
qrCode: string;
@IsNotEmpty()
@IsBase32()
secret: string;
@IsNotEmpty()
@IsBase32()
secret: string;
}
export class TfaRequiredResponse extends SuccessResponse {}

View File

@ -0,0 +1,25 @@
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

@ -0,0 +1,89 @@
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

@ -0,0 +1,5 @@
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

@ -0,0 +1,27 @@
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 {}

41
frontend/src/dto/utils.ts Normal file
View File

@ -0,0 +1,41 @@
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<T extends { new (...args: any[]): any }>(
constr: T
) {
return class extends constr {
constructor(...args: any[]) {
super(...args);
validateSync(this);
}
};
}

View File

@ -1,8 +1,8 @@
import { createApp } from 'vue';
import router from './router';
import AppAsyncWrapper from './AppAsyncWrapper.vue';
import { createApp } from "vue";
import router from "./router";
import AppAsyncWrapper from "./AppAsyncWrapper.vue";
const app = createApp(AppAsyncWrapper);
app.use(router);
app.config.unwrapInjectedRef = true;
app.mount('#app');
app.mount("#app");

View File

@ -1,63 +1,63 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import LoginView from '@/views/LoginView.vue';
import SignupView from '@/views/SignupView.vue';
import HomeView from '@/views/HomeView.vue';
import AboutView from '@/views/AboutView.vue';
import FSView from '@/views/FSView.vue';
import SetTokenView from '@/views/SetTokenView.vue';
import ProfileView from '@/views/ProfileView.vue';
import TFAView from '@/views/TFAView.vue';
import AdminView from '@/views/AdminView.vue';
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import LoginView from "@/views/LoginView.vue";
import SignupView from "@/views/SignupView.vue";
import HomeView from "@/views/HomeView.vue";
import AboutView from "@/views/AboutView.vue";
import FSView from "@/views/FSView.vue";
import SetTokenView from "@/views/SetTokenView.vue";
import ProfileView from "@/views/ProfileView.vue";
import TFAView from "@/views/TFAView.vue";
import AdminView from "@/views/AdminView.vue";
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/profile',
name: 'profile',
component: ProfileView
},
{
path: '/profile/2fa-enable',
name: '2fa',
component: TFAView
},
{
path: '/admin',
component: AdminView
},
{
path: '/about',
component: AboutView
},
{
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/signup',
name: 'signup',
component: SignupView
},
{
path: '/fs/:node_id',
name: 'fs',
component: FSView
},
{
path: "/",
name: "home",
component: HomeView,
},
{
path: "/profile",
name: "profile",
component: ProfileView,
},
{
path: "/profile/2fa-enable",
name: "2fa",
component: TFAView,
},
{
path: "/admin",
component: AdminView,
},
{
path: "/about",
component: AboutView,
},
{
path: "/login",
name: "login",
component: LoginView,
},
{
path: "/signup",
name: "signup",
component: SignupView,
},
{
path: "/fs/:node_id",
name: "fs",
component: FSView,
},
{
path: '/set_token',
component: SetTokenView
}
{
path: "/set_token",
component: SetTokenView,
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;

View File

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

View File

@ -1,109 +1,109 @@
<script setup lang="ts">
import { inject, onBeforeMount, ref } from 'vue';
import { inject, onBeforeMount, ref } from "vue";
import {
Responses,
check_token,
TokenInjectType,
Admin,
isErrorResponse
} from '@/api';
import { onBeforeRouteUpdate } from 'vue-router';
import router from '@/router';
Responses,
check_token,
TokenInjectType,
Admin,
isErrorResponse,
} from "@/api";
import { onBeforeRouteUpdate } from "vue-router";
import router from "@/router";
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const users = ref<Responses.Admin.GetUsersEntry[]>([]);
onBeforeRouteUpdate(async () => {
await updatePanel();
await updatePanel();
});
onBeforeMount(async () => {
await updatePanel();
await updatePanel();
});
async function updatePanel() {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await Admin.get_users(token);
if (isErrorResponse(res)) return router.replace({ path: '/' });
users.value = res.users;
const res = await Admin.get_users(token);
if (isErrorResponse(res)) return router.replace({ path: "/" });
users.value = res.users;
}
async function setRole(user: number, roleStr: string) {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await Admin.set_role(user, parseInt(roleStr, 10), token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
const res = await Admin.set_role(user, parseInt(roleStr, 10), token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
}
async function disableTfa(user: number) {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await Admin.disable_tfa(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
const res = await Admin.disable_tfa(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
}
async function logoutUser(user: number) {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await Admin.logout(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
const res = await Admin.logout(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
}
async function deleteUser(user: number) {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await Admin.delete_user(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
const res = await Admin.delete_user(user, token);
if (isErrorResponse(res)) console.error(res.message);
await updatePanel();
}
</script>
<template>
<table>
<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.value)">
<option value="0" :selected="user.role === 0 ? true : null">
Disabled
</option>
<option value="1" :selected="user.role === 1 ? true : null">
User
</option>
<option value="2" :selected="user.role === 2 ? true : null">
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>
<table>
<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.value)">
<option value="0" :selected="user.role === 0 ? true : null">
Disabled
</option>
<option value="1" :selected="user.role === 1 ? true : null">
User
</option>
<option value="2" :selected="user.role === 2 ? true : null">
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>
<style scoped></style>

View File

@ -1,70 +1,70 @@
<script setup lang="ts">
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
import { inject, onBeforeMount, ref } from 'vue';
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
import { inject, onBeforeMount, ref } from "vue";
import {
check_token,
FS,
Responses,
isErrorResponse,
TokenInjectType
} from '@/api';
import DirViewer from '@/components/FSView/DirViewer.vue';
import FileViewer from '@/components/FSView/FileViewer.vue';
check_token,
FS,
Responses,
isErrorResponse,
TokenInjectType,
} from "@/api";
import DirViewer from "@/components/FSView/DirViewer.vue";
import FileViewer from "@/components/FSView/FileViewer.vue";
const router = useRouter();
const route = useRoute();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const path = ref('');
const path = ref("");
const node = ref<Responses.FS.GetNodeResponse | null>(null);
async function fetch_node(node_id: number) {
const token = await check_token(jwt);
if (!token) return;
let [p, n] = [
await FS.get_path(token, node_id),
await FS.get_node(token, node_id)
];
if (isErrorResponse(p)) return gotoRoot();
if (isErrorResponse(n)) return gotoRoot();
[path.value, node.value] = [p.path, n];
const token = await check_token(jwt);
if (!token) return;
let [p, n] = [
await FS.get_path(token, node_id),
await FS.get_node(token, node_id),
];
if (isErrorResponse(p)) return gotoRoot();
if (isErrorResponse(n)) return gotoRoot();
[path.value, node.value] = [p.path, n];
}
onBeforeRouteUpdate(async (to) => {
await fetch_node(Number(to.params.node_id));
await fetch_node(Number(to.params.node_id));
});
async function reloadNode() {
await fetch_node(Number(route.params.node_id));
await fetch_node(Number(route.params.node_id));
}
onBeforeMount(async () => {
await reloadNode();
await reloadNode();
});
async function gotoRoot() {
const token = await check_token(jwt);
if (!token) return;
const rootRes = await FS.get_root(token);
if (isErrorResponse(rootRes)) return jwt.logout();
const root = rootRes.rootId;
await router.replace({
name: 'fs',
params: { node_id: root }
});
const token = await check_token(jwt);
if (!token) return;
const rootRes = await FS.get_root(token);
if (isErrorResponse(rootRes)) return jwt.logout();
const root = rootRes.rootId;
await router.replace({
name: "fs",
params: { node_id: root },
});
}
</script>
<template>
<div v-if="node">
<div>Path: {{ path }}</div>
<DirViewer
v-if="!node.isFile"
:node="node"
@reloadNode="reloadNode"
@gotoRoot="gotoRoot"
/>
<FileViewer v-else :node="node" />
</div>
<div v-if="node">
<div>Path: {{ path }}</div>
<DirViewer
v-if="!node.isFile"
:node="node"
@reloadNode="reloadNode"
@gotoRoot="gotoRoot"
/>
<FileViewer v-else :node="node" />
</div>
</template>
<style scoped></style>

View File

@ -1,28 +1,28 @@
<template><p></p></template>
<script setup lang="ts">
import { onBeforeRouteUpdate, useRouter } from 'vue-router';
import { inject, onBeforeMount } from 'vue';
import { FS, check_token, isErrorResponse, TokenInjectType } from '@/api';
import { onBeforeRouteUpdate, useRouter } from "vue-router";
import { inject, onBeforeMount } from "vue";
import { FS, check_token, isErrorResponse, TokenInjectType } from "@/api";
const router = useRouter();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function start_redirect() {
const token = await check_token(jwt);
if (!token) return;
const root = await FS.get_root(token);
if (isErrorResponse(root)) return jwt.logout();
await router.replace({
name: 'fs',
params: { node_id: root.rootId }
});
const token = await check_token(jwt);
if (!token) return;
const root = await FS.get_root(token);
if (isErrorResponse(root)) return jwt.logout();
await router.replace({
name: "fs",
params: { node_id: root.rootId },
});
}
onBeforeRouteUpdate(async () => {
await start_redirect();
await start_redirect();
});
onBeforeMount(async () => {
await start_redirect();
await start_redirect();
});
</script>

View File

@ -1,61 +1,61 @@
<script setup lang="ts">
import { ref, inject } from 'vue';
import { Auth, FS, isErrorResponse, TokenInjectType } from '@/api';
import { useRouter } from 'vue-router';
import { ref, inject } from "vue";
import { Auth, FS, isErrorResponse, TokenInjectType } from "@/api";
import { useRouter } from "vue-router";
const router = useRouter();
const username = ref('');
const password = ref('');
const otp = ref('');
const username = ref("");
const password = ref("");
const otp = ref("");
const error = ref('');
const error = ref("");
const requestOtp = ref(false);
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function login() {
error.value = '';
if (username.value === '' || password.value === '') {
error.value = 'Email and/or Password missing';
return;
}
const res = await (requestOtp.value
? Auth.auth_login(username.value, password.value, otp.value)
: Auth.auth_login(username.value, password.value));
if (isErrorResponse(res)) error.value = 'Login failed: ' + res.message;
else if ('jwt' in res) {
const root = await FS.get_root(res.jwt);
if (isErrorResponse(root)) {
error.value = 'Get root failed: ' + root.message;
return;
}
jwt.setToken(res.jwt);
await router.push({
name: 'fs',
params: { node_id: root.rootId }
});
} else {
error.value = '';
requestOtp.value = true;
}
error.value = "";
if (username.value === "" || password.value === "") {
error.value = "Email and/or Password missing";
return;
}
const res = await (requestOtp.value
? Auth.auth_login(username.value, password.value, otp.value)
: Auth.auth_login(username.value, password.value));
if (isErrorResponse(res)) error.value = "Login failed: " + res.message;
else if ("jwt" in res) {
const root = await FS.get_root(res.jwt);
if (isErrorResponse(root)) {
error.value = "Get root failed: " + root.message;
return;
}
jwt.setToken(res.jwt);
await router.push({
name: "fs",
params: { node_id: root.rootId },
});
} else {
error.value = "";
requestOtp.value = true;
}
}
</script>
<template>
<div v-if="error !== ''" v-text="error"></div>
<template v-if="!requestOtp">
<input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" />
<a href="/api/auth/gitlab">Login with gitlab</a>
<router-link to="signup">Signup instead?</router-link>
</template>
<template v-else>
<div>Please input your 2 factor authentication code</div>
<input type="text" placeholder="Code" v-model="otp" />
</template>
<button @click="login()">Login</button>
<div v-if="error !== ''" v-text="error"></div>
<template v-if="!requestOtp">
<input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" />
<a href="/api/auth/gitlab">Login with gitlab</a>
<router-link to="signup">Signup instead?</router-link>
</template>
<template v-else>
<div>Please input your 2 factor authentication code</div>
<input type="text" placeholder="Code" v-model="otp" />
</template>
<button @click="login()">Login</button>
</template>
<style scoped></style>

View File

@ -1,124 +1,112 @@
<script setup lang="ts">
import { ref, inject, onBeforeMount } from 'vue';
import { ref, inject, onBeforeMount } from "vue";
import {
Auth,
User,
check_token,
isErrorResponse,
TokenInjectType,
Responses
} from '@/api';
import { onBeforeRouteUpdate } from 'vue-router';
Auth,
User,
check_token,
isErrorResponse,
TokenInjectType,
Responses,
} from "@/api";
import { onBeforeRouteUpdate } from "vue-router";
const error = ref('');
const oldPw = ref('');
const newPw = ref('');
const newPw2 = ref('');
const error = ref("");
const oldPw = ref("");
const newPw = ref("");
const newPw2 = ref("");
const user = ref<Responses.User.UserInfoResponse | null>(null);
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
onBeforeRouteUpdate(async () => {
await updateProfile();
await updateProfile();
});
onBeforeMount(async () => {
await updateProfile();
await updateProfile();
});
async function updateProfile() {
const token = await check_token(jwt);
if (!token) return;
const token = await check_token(jwt);
if (!token) return;
const res = await User.get_user_info(token);
if (isErrorResponse(res)) return jwt.logout();
user.value = res;
const res = await User.get_user_info(token);
if (isErrorResponse(res)) return jwt.logout();
user.value = res;
}
async function deleteUser() {
const token = await check_token(jwt);
if (!token) return;
await User.delete_user(token);
jwt.logout();
const token = await check_token(jwt);
if (!token) return;
await User.delete_user(token);
jwt.logout();
}
async function logoutAll() {
const token = await check_token(jwt);
if (!token) return;
await Auth.logout_all(token);
jwt.logout();
const token = await check_token(jwt);
if (!token) return;
await Auth.logout_all(token);
jwt.logout();
}
async function changePw() {
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();
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);
if (!token) return;
await Auth.tfa_disable(token);
jwt.logout();
const token = await check_token(jwt);
if (!token) return;
await Auth.tfa_disable(token);
jwt.logout();
}
</script>
<template>
<template v-if="user">
<div v-if="error !== ''" v-text="error"></div>
<div>User: {{ user.name }}</div>
<div>Signed in with {{ user.gitlab ? 'gitlab' : 'password' }}</div>
<template v-if="!user.gitlab">
<div>
<input
type="password"
placeholder="Old password"
v-model="oldPw"
/>
<input
type="password"
placeholder="New password"
v-model="newPw"
/>
<input
type="password"
placeholder="Repeat new password"
v-model="newPw2"
/>
<button @click="changePw">Change</button>
</div>
<div>
<div>
2 Factor authentication:
{{ user.tfaEnabled ? 'Enabled' : 'Disabled' }}
</div>
<div>
<a href="#" v-if="user.tfaEnabled" @click="tfaDisable">
Disable
</a>
<router-link to="/profile/2fa-enable" v-else>
Enable
</router-link>
</div>
</div>
</template>
<div>
<a href="#" @click="logoutAll">Logout everywhere</a>
<a href="#" @click="deleteUser">Delete Account</a>
</div>
</template>
<template v-else>
<div>Loading...</div>
</template>
<template v-if="user">
<div v-if="error !== ''" v-text="error"></div>
<div>User: {{ user.name }}</div>
<div>Signed in with {{ user.gitlab ? "gitlab" : "password" }}</div>
<template v-if="!user.gitlab">
<div>
<input type="password" placeholder="Old password" v-model="oldPw" />
<input type="password" placeholder="New password" v-model="newPw" />
<input
type="password"
placeholder="Repeat new password"
v-model="newPw2"
/>
<button @click="changePw">Change</button>
</div>
<div>
<div>
2 Factor authentication:
{{ user.tfaEnabled ? "Enabled" : "Disabled" }}
</div>
<div>
<a href="#" v-if="user.tfaEnabled" @click="tfaDisable"> Disable </a>
<router-link to="/profile/2fa-enable" v-else> Enable </router-link>
</div>
</div>
</template>
<div>
<a href="#" @click="logoutAll">Logout everywhere</a>
<a href="#" @click="deleteUser">Delete Account</a>
</div>
</template>
<template v-else>
<div>Loading...</div>
</template>
</template>
<style scoped></style>

View File

@ -1,19 +1,19 @@
<script setup lang="ts">
import { inject } from 'vue';
import { TokenInjectType } from '@/api';
import { useRoute, useRouter } from 'vue-router';
import { inject } from "vue";
import { TokenInjectType } from "@/api";
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
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);
router.replace({ path: '/' });
if ("token" in route.query) jwt.setToken(route.query["token"] as string);
router.replace({ path: "/" });
</script>
<template>
<router-link to="/">Click here to go home</router-link>
<router-link to="/">Click here to go home</router-link>
</template>
<style scoped></style>

View File

@ -1,35 +1,35 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Auth, isErrorResponse } from '@/api';
import { ref } from "vue";
import { Auth, isErrorResponse } from "@/api";
const username = ref('');
const password = ref('');
const password2 = ref('');
const error = ref('');
const username = ref("");
const password = ref("");
const password2 = ref("");
const error = ref("");
async function signup() {
if (username.value === '' || password.value === '') {
error.value = 'Email and/or Password missing';
return;
}
if (password.value !== password2.value) {
error.value = "Passwords don't match";
return;
}
const res = await Auth.auth_signup(username.value, password.value);
error.value = isErrorResponse(res)
? 'Signup failed: ' + res.message
: 'Signup successful, please wait till an admin unlocks your account.';
if (username.value === "" || password.value === "") {
error.value = "Email and/or Password missing";
return;
}
if (password.value !== password2.value) {
error.value = "Passwords don't match";
return;
}
const res = await Auth.auth_signup(username.value, password.value);
error.value = isErrorResponse(res)
? "Signup failed: " + res.message
: "Signup successful, please wait till an admin unlocks your account.";
}
</script>
<template>
<div v-if="error !== ''" v-text="error"></div>
<input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" />
<input type="password" placeholder="Repeat password" v-model="password2" />
<button @click="signup()">Signup</button>
<router-link to="login">Login instead?</router-link>
<div v-if="error !== ''" v-text="error"></div>
<input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" />
<input type="password" placeholder="Repeat password" v-model="password2" />
<button @click="signup()">Signup</button>
<router-link to="login">Login instead?</router-link>
</template>
<style scoped></style>

View File

@ -1,89 +1,89 @@
<script setup lang="ts">
import { ref, inject } from 'vue';
import { Auth, check_token, isErrorResponse, TokenInjectType } from '@/api';
import { ref, inject } from "vue";
import { Auth, check_token, isErrorResponse, TokenInjectType } from "@/api";
enum state {
SELECT,
MAIL,
TOTP
SELECT,
MAIL,
TOTP,
}
const currentState = ref<state>(state.SELECT);
const error = ref('');
const qrImage = ref('');
const secret = ref('');
const code = ref('');
const error = ref("");
const qrImage = ref("");
const secret = ref("");
const code = ref("");
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function selectMail() {
const token = await check_token(jwt);
if (!token) return;
error.value = 'Working...';
const res = await Auth.tfa_setup(true, token);
if (isErrorResponse(res))
error.value = 'Failed to select 2fa type: ' + res.message;
else {
error.value = '';
currentState.value = state.MAIL;
}
const token = await check_token(jwt);
if (!token) return;
error.value = "Working...";
const res = await Auth.tfa_setup(true, token);
if (isErrorResponse(res))
error.value = "Failed to select 2fa type: " + res.message;
else {
error.value = "";
currentState.value = state.MAIL;
}
}
async function selectTotp() {
const token = await check_token(jwt);
if (!token) return;
error.value = 'Working...';
const res = await Auth.tfa_setup(false, token);
if (isErrorResponse(res))
error.value = 'Failed to select 2fa type: ' + res.message;
else {
qrImage.value = res.qrCode;
secret.value = res.secret;
error.value = '';
currentState.value = state.TOTP;
}
const token = await check_token(jwt);
if (!token) return;
error.value = "Working...";
const res = await Auth.tfa_setup(false, token);
if (isErrorResponse(res))
error.value = "Failed to select 2fa type: " + res.message;
else {
qrImage.value = res.qrCode;
secret.value = res.secret;
error.value = "";
currentState.value = state.TOTP;
}
}
async function submit() {
const token = await check_token(jwt);
if (!token) return;
error.value = 'Working...';
const res = await Auth.tfa_complete(
currentState.value === state.MAIL,
code.value,
token
);
if (isErrorResponse(res))
error.value = 'Failed to submit code: ' + res.message;
else jwt.logout();
const token = await check_token(jwt);
if (!token) return;
error.value = "Working...";
const res = await Auth.tfa_complete(
currentState.value === state.MAIL,
code.value,
token
);
if (isErrorResponse(res))
error.value = "Failed to submit code: " + res.message;
else jwt.logout();
}
</script>
<template>
<div v-if="error !== ''" v-text="error"></div>
<template v-if="currentState === state.SELECT">
<div>Select 2 Factor authentication type:</div>
<div>
<button @click="selectMail">Mail</button>
<button @click="selectTotp">Google Authenticator</button>
</div>
</template>
<template v-else-if="currentState === state.MAIL">
<div>Please enter the code you got by mail</div>
<input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button>
</template>
<template v-else>
<img :src="qrImage" alt="QrCode" />
<details>
<summary>Show manual input code</summary>
{{ secret }}
</details>
<div>Please enter the current code</div>
<input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button>
</template>
<div v-if="error !== ''" v-text="error"></div>
<template v-if="currentState === state.SELECT">
<div>Select 2 Factor authentication type:</div>
<div>
<button @click="selectMail">Mail</button>
<button @click="selectTotp">Google Authenticator</button>
</div>
</template>
<template v-else-if="currentState === state.MAIL">
<div>Please enter the code you got by mail</div>
<input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button>
</template>
<template v-else>
<img :src="qrImage" alt="QrCode" />
<details>
<summary>Show manual input code</summary>
{{ secret }}
</details>
<div>Please enter the current code</div>
<input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button>
</template>
</template>
<style scoped></style>

View File

@ -1,12 +1,12 @@
const { defineConfig } = require('@vue/cli-service');
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
resolve: {
fallback: {
crypto: false,
stream: require.resolve('stream-browserify')
}
}
}
transpileDependencies: true,
configureWebpack: {
resolve: {
fallback: {
crypto: false,
stream: require.resolve("stream-browserify"),
},
},
},
});

29
old_backend/.eslintrc.js Normal file
View File

@ -0,0 +1,29 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin', 'no-relative-import-paths'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-relative-import-paths/no-relative-import-paths': [
'error',
{ 'allowSameFolder': true, 'rootDir': 'src' }
]
},
};

401
old_backend/.gitignore vendored Normal file
View File

@ -0,0 +1,401 @@
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### VisualStudio template
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
coverage/
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
=======
# Local
.env
dist
files
sqlite.db

114
old_backend/.gitlab-ci.yml Normal file
View File

@ -0,0 +1,114 @@
image: node:latest
stages:
- setup
- test
- build
- package
cache: &global_cache
paths:
- .yarn
- node_modules
- frontend/.yarn
- frontend/node_modules
policy: pull
before_script:
- yarn install --cache-folder .yarn --frozen-lockfile
- cd frontend
- yarn install --cache-folder .yarn --frozen-lockfile
- cd ..
.dto_artifacts_need: &dto_artifacts_need
job: test_build_dto
artifacts: true
test_build_dto:
stage: setup
cache:
<<: *global_cache
policy: pull-push
before_script: []
script:
- cd dto
- yarn install --frozen-lockfile
- yarn lint
- yarn build
- cd ..
- yarn install --cache-folder .yarn --frozen-lockfile
- yarn add ./dto
- cd frontend
- yarn install --cache-folder .yarn --frozen-lockfile
- yarn add ../dto
artifacts:
paths:
- dto/lib/
test_backend:
needs:
- *dto_artifacts_need
stage: test
script:
- yarn lint
test_frontend:
needs:
- *dto_artifacts_need
stage: test
script:
- cd frontend
- yarn lint
build_backend:
stage: build
needs:
- *dto_artifacts_need
- job: test_backend
artifacts: false
script:
- echo This has to work till I rewrite the backend
- false && echo
- yarn webpack
artifacts:
paths:
- dist/
expire_in: 1h
build_frontend:
stage: build
needs:
- *dto_artifacts_need
- job: test_frontend
artifacts: false
script:
- cd frontend
- yarn build
artifacts:
paths:
- frontend/dist/
expire_in: 1h
package_server:
stage: package
cache: []
before_script: []
needs:
- job: build_backend
artifacts: true
- job: build_frontend
artifacts: true
script:
- TMP=$(mktemp -d)
- mv dist/* "$TMP"
- mkdir "$TMP/frontend"
- mv frontend/dist/* "$TMP/frontend"
- rm -r *
- rm -r .* || true
- mv "$TMP/"* .
artifacts:
paths:
- package.json
- server.js
- frontend/

7
old_backend/.prettierrc Normal file
View File

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

19
old_backend/README.md Normal file
View File

@ -0,0 +1,19 @@
# Mutzi's fileserver
## Description
The most crackhead fileserver you will find on the market
## Installation
```bash
npm install
cd frontend && npm install
```
## Running the app
```bash
npm run start:dev
```
Run in parallel for building the frontend:
````bash
cd frontend && npm run serve
````

View File

@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"monorepo": true,
"sourceRoot": "src",
"compilerOptions": {
"tsConfigPath": "tsconfig.json"
}
}

122
old_backend/package.json Normal file
View File

@ -0,0 +1,122 @@
{
"name": "fileserver",
"private": true,
"version": "1.0.0",
"description": "Crackhead fileserver",
"license": "MIT",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"lint": "eslint \"src/**/*.ts\"",
"lint-fix": "eslint \"src/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"genapi": "ts-node tools/apigen.ts",
"updateDto": "cd dto && yarn build && cd .. && yarn add ./dto && cd frontend && yarn add ../dto",
"lint-fix-all": "yarn lint-fix && cd dto && yarn lint-fix && cd ../frontend && yarn lint --fix"
},
"dependencies": {
"@fastify/multipart": "^7.1.0",
"@fastify/static": "^6.5.0",
"@nestjs/common": "^9.0.8",
"@nestjs/core": "^9.0.8",
"@nestjs/jwt": "^9.0.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-fastify": "^9.0.8",
"@nestjs/serve-static": "^3.0.0",
"@nestjs/typeorm": "^9.0.0",
"argon2": "^0.28.7",
"axios": "^0.27.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.7.8",
"notp": "^2.0.3",
"passport": "^0.6.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"qrcode": "^1.5.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.5.6",
"sqlite3": "^5.0.11",
"thirty-two": "^1.0.2",
"typeorm": "^0.3.7"
},
"runtimeDependencies": [
"@fastify/multipart",
"@fastify/static",
"@nestjs/common",
"@nestjs/core",
"@nestjs/platform-fastify",
"@nestjs/serve-static",
"argon2",
"class-transformer",
"class-validator",
"reflect-metadata",
"rxjs",
"sqlite3",
"typeorm"
],
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.1",
"@nestjs/testing": "^9.0.8",
"@types/express": "^4.17.13",
"@types/jest": "^28.1.6",
"@types/jsonwebtoken": "^8.5.8",
"@types/node": "^18.6.5",
"@types/nodemailer": "^6.4.5",
"@types/notp": "^2.0.2",
"@types/passport-jwt": "^3.0.6",
"@types/passport-local": "^1.0.34",
"@types/qrcode": "^1.5.0",
"@types/supertest": "^2.0.12",
"@types/webpack": "^5.28.0",
"@types/webpack-node-externals": "^2.5.3",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"@typescript-eslint/typescript-estree": "^5.33.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-no-relative-import-paths": "^1.4.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^28.1.3",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"source-map-support": "^0.5.21",
"supertest": "^6.2.4",
"ts-jest": "^28.0.7",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.7.4",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-node-externals": "^3.0.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

36
old_backend/requests.http Normal file
View File

@ -0,0 +1,36 @@
### Create account
POST http://127.0.0.1:8080/api/auth/signup
Content-Type: application/json
{"username": "root@mattv.de", "password": "123"}
### Wrong authenctication
POST http://127.0.0.1:8080/api/auth/login
Content-Type: application/json
{"username": "root@mattv.de", "password": "this is not correct"}
### Correct authentication
POST http://127.0.0.1:8080/api/auth/login
Content-Type: application/json
{"username": "root@mattv.de", "password": "123"}
> {% client.global.set("auth_token", response.body.jwt); %}
### Check if authenticated with admin perms
GET http://127.0.0.1:8080/test/hello2
Authorization: Bearer {{auth_token}}
### Refresh token
POST http://127.0.0.1:8080/api/auth/refresh
Authorization: Bearer {{auth_token}}
### A
POST https://ssh.gitlab.mattv.de/oauth/token
?redirect_uri=http%3A//127.0.0.1%3A1234/api/auth/gitlab_callback
&client_id=98bcbad78cb1f880d1d1de62291d70a791251a7bea077bfe7df111ef3c115760
&client_secret=7ee01d2b204aff3a05f9d028f004d169b6d381ec873e195f314b3935fa150959
&code=b96f91b171cf23245ea08c6f35c3831698e8683c6d0306de1396507f0f51d4c7
&grant_type=authorization_code

View File

@ -0,0 +1,28 @@
import {
Body,
Controller,
Get,
Param,
ParseIntPipe,
Post,
Request,
StreamableFile,
ValidationPipe
} from '@nestjs/common';
import { Responses, Requests, validateAsyncInline, UserRole } from '../../dto';
import FileSystemService from 'services/filesystem';
import { Role } from 'authguards';
@Controller('api/fs')
export default class FileSystemController {
constructor(private fsService: FileSystemService) {}
@Post('download')
@Role(UserRole.USER)
async download(
@Request() req,
@Body('id', ParseIntPipe) id
): Promise<StreamableFile> {
return this.fsService.downloadFile(id, req.user);
}
}

View File

@ -0,0 +1,32 @@
import {
BadRequestException,
Injectable,
NotImplementedException,
StreamableFile,
UnauthorizedException
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { INode, User } from 'entities';
import { Repository } from 'typeorm';
import { Multipart } from '@fastify/multipart';
import { pipeline } from 'stream/promises';
import { createReadStream, createWriteStream, statSync, unlink } from 'fs';
import { Writable } from 'stream';
@Injectable()
export default class FileSystemService {
constructor(
@InjectRepository(INode)
private inodeRepo: Repository<INode>
) {}
async downloadFile(id: number, user: User): Promise<StreamableFile> {
const node = await this.getNodeAndValidate(id, user);
if (!node.isFile) throw new NotImplementedException();
const stats = statSync(`files/${node.id}`);
return new StreamableFile(createReadStream(`files/${node.id}`), {
disposition: `attachment; filename="${node.name}"`,
length: stats.size
});
}
}

View File

@ -35,10 +35,16 @@ class BetterWritable {
this.indent--;
this.write(data);
}
writeDecInc(data: string) {
this.indent--;
this.write(data);
this.indent++;
}
}
interface Outputable {
write(s: BetterWritable);
writeJsonAs(ns: string, s: BetterWritable);
}
class CppProp {
@ -48,23 +54,55 @@ class CppProp {
entry: string;
}
// template <> inline dto::Requests::Auth::LoginRequest Json::Value::as<dto::Requests::Auth::LoginRequest>() const { return asFloat(); }
class CppClass implements Outputable {
constructor(name: string) {
this.name = name;
}
type = 'class' as const;
name: string;
sub: string[] = [];
entries: CppProp[] = [];
write(s: BetterWritable) {
let name = `struct ${this.name}`;
if (this.sub.length > 0) name += ' : ' + this.sub.join(', ');
if (this.entries.length == 0) s.write(`${name} {}`);
else {
s.writeInc(`${name} {`);
this.entries.forEach((e) => s.write(`${e.entry};`));
s.writeDec('};');
if (this.sub.length > 0)
name += ' : public ' + this.sub.join(', public ');
s.writeInc(`${name} {`);
if (this.sub.length == 0) {
s.write(`${this.name}() = default;`);
s.writeInc(`explicit ${this.name}(const Json::Value& j) {`);
} else {
s.writeInc(`${this.name}() :`);
this.sub.forEach((sub) => {
s.write(`${sub}()`);
});
s.writeDec('{}');
s.writeInc(`explicit ${this.name}(const Json::Value& j) :`);
this.sub.forEach((sub) => {
s.write(`${sub}(j)`);
});
s.writeDecInc('{');
}
this.entries.forEach((e) => {
const [type, name] = e.entry.split(' ');
const opt_match = /std::optional<([a-z:<>]+)>/.exec(type);
if (opt_match !== null)
s.write(
`this->${name} = json_get_optional<${opt_match[1]}>(j, "${name}");`
);
else s.write(`this->${name} = j["${name}"].as<${type}>();`);
});
s.writeDec('}');
this.entries.forEach((e) => s.write(`${e.entry};`));
s.writeDec('};');
}
writeJsonAs(ns: string, s: BetterWritable) {
s.write(
`template <> inline ${ns}${this.name} Json::Value::as() const { return ${ns}${this.name}(*this); }`
);
}
}
@ -72,22 +110,32 @@ class CppEnum implements Outputable {
constructor(name: string) {
this.name = name;
}
type = 'enum' as const;
name: string;
entries: CppProp[] = [];
write(s: BetterWritable) {
s.writeInc(`enum ${this.name} {`);
return;
s.writeInc(`enum ${this.name} : int {`);
this.entries.forEach((e, idx, arr) =>
s.write(`${e.entry}${idx === arr.length - 1 ? '' : ','}`)
);
s.writeDec('};');
}
writeJsonAs(ns: string, s: BetterWritable) {
return;
s.write(
`template <> inline ${ns}${this.name} Json::Value::as() const { return (${ns}${this.name})(asInt()); }`
);
}
}
class CppNamespace implements Outputable {
constructor(name: string) {
this.name = name;
}
type = 'ns' as const;
name: string;
body: (CppNamespace | CppClass | CppEnum)[] = [];
@ -96,56 +144,57 @@ class CppNamespace implements Outputable {
this.body.forEach((b) => b.write(s));
s.writeDec('}');
}
writeJsonAs(ns: string, s: BetterWritable) {
ns += `${this.name}::`;
this.body.forEach((b) => b.writeJsonAs(ns, s));
}
}
function _getCppType(
t: TypeNode
): string | ((opt: boolean, name: string) => string) {
): null | string | ((opt: boolean, name: string) => string) {
switch (t.type) {
case AST_NODE_TYPES.TSStringKeyword:
return 'std::string';
case AST_NODE_TYPES.TSBooleanKeyword:
return 'bool';
case AST_NODE_TYPES.TSNumberKeyword:
return 'long';
return 'int';
case AST_NODE_TYPES.TSTypeReference:
if (t.typeName.type != AST_NODE_TYPES.Identifier) throw new Err(t);
return t.typeName.name;
case AST_NODE_TYPES.TSLiteralType:
if (t.literal.type != AST_NODE_TYPES.Literal) throw new Err(t);
return (opt, name) => `${name} = ${(t.literal as a.Literal).raw}`;
return null;
case AST_NODE_TYPES.TSUnionType:
return _getCppType(t.types[0]);
case AST_NODE_TYPES.TSArrayType:
return (opt, name) =>
opt
? `std::optional<${_getCppType(t.elementType)}[]> ${name}`
: `${getCppType(false, name, t.elementType)}[]`;
return `std::vector<${_getCppType(t.elementType)}>`;
default:
throw new Err(t);
}
}
function getCppType(opt: boolean, name: string, t: TypeNode): string {
function getCppType(opt: boolean, name: string, t: TypeNode): null | string {
let ret = _getCppType(t);
if (typeof ret === 'function') return `${ret(opt, name)}`;
if (typeof ret === 'string') {
if (opt) ret = `std::optional<${ret}>`;
return `${ret} ${name}`;
} else return `${ret(opt, name)}`;
} else return ret;
}
function handleClassProperty(
prop: a.PropertyDefinitionComputedName | a.PropertyDefinitionNonComputedName
): CppProp {
): CppProp | null {
if (prop.key.type != AST_NODE_TYPES.Identifier) throw new Err(prop);
if (!prop.typeAnnotation) throw new Err(prop);
return new CppProp(
getCppType(
prop.optional && prop.optional,
prop.key.name,
prop.typeAnnotation.typeAnnotation
)
const type = getCppType(
prop.optional && prop.optional,
prop.key.name,
prop.typeAnnotation.typeAnnotation
);
return type === null ? null : new CppProp(type);
}
function handleClass(
@ -160,13 +209,14 @@ function handleClass(
}
dec.body.body.forEach((inner) => {
if (inner.type == AST_NODE_TYPES.PropertyDefinition) {
cls.entries.push(handleClassProperty(inner));
const prop = handleClassProperty(inner);
if (prop) cls.entries.push(prop);
} else if (
inner.type == AST_NODE_TYPES.MethodDefinition &&
inner.key.type == AST_NODE_TYPES.Identifier &&
inner.key.name === 'constructor'
)
console.warn('Handle constructors?');
return;
else throw new Err(inner);
});
return cls;
@ -240,8 +290,17 @@ function parseFile(ns: CppNamespace, file: string) {
}
const globalNamespace = new CppNamespace('dto');
parseFile(globalNamespace, 'dto/index.ts');
parseFile(globalNamespace, '../dto/index.ts');
const output_stream = fs.createWriteStream('dto.h');
output_stream.write('#include <string>\n#include <optional>\n\n');
globalNamespace.write(new BetterWritable(output_stream));
const output_stream = fs.createWriteStream('../backend/src/dto.h');
output_stream.write(
'#pragma once\n\n' +
'#include <string>\n' +
'#include <optional>\n' +
'#include <vector>\n' +
'#include <json/allocator.h>\n' +
'#include "dto_extras.h"\n\n'
);
const output = new BetterWritable(output_stream);
globalNamespace.write(output);
globalNamespace.writeJsonAs('', output);

19
old_backend/tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./src",
"incremental": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"strictPropertyInitialization": false
},
"exclude": ["node_modules", "dist", "test", "**/*spec.ts", "frontend"]
}

6014
old_backend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff