Rewrote backend in c++
This commit is contained in:
88
backend/src/controllers/admin.cpp
Normal file
88
backend/src/controllers/admin.cpp
Normal 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
|
||||
103
backend/src/controllers/auth/auth_2fa.cpp
Normal file
103
backend/src/controllers/auth/auth_2fa.cpp
Normal 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
|
||||
135
backend/src/controllers/auth/auth_basic.cpp
Normal file
135
backend/src/controllers/auth/auth_basic.cpp
Normal 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
|
||||
110
backend/src/controllers/auth/auth_common.cpp
Normal file
110
backend/src/controllers/auth/auth_common.cpp
Normal 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
|
||||
118
backend/src/controllers/auth/auth_gitlab.cpp
Normal file
118
backend/src/controllers/auth/auth_gitlab.cpp
Normal 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
|
||||
119
backend/src/controllers/controllers.h
Normal file
119
backend/src/controllers/controllers.h
Normal 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
|
||||
211
backend/src/controllers/fs.cpp
Normal file
211
backend/src/controllers/fs.cpp
Normal 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
|
||||
29
backend/src/controllers/user.cpp
Normal file
29
backend/src/controllers/user.cpp
Normal 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
|
||||
Reference in New Issue
Block a user