Add Visione backend foundation
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
#include "common/Auth.h"
|
||||
|
||||
#include "common/Crypto.h"
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
namespace visione::auth
|
||||
{
|
||||
std::optional<std::string> bearerToken(const drogon::HttpRequestPtr &request)
|
||||
{
|
||||
const auto header = request->getHeader("Authorization");
|
||||
constexpr std::string_view prefix = "Bearer ";
|
||||
if (!header.starts_with(prefix))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return header.substr(prefix.size());
|
||||
}
|
||||
|
||||
std::optional<UserContext> authenticate(const drogon::HttpRequestPtr &request)
|
||||
{
|
||||
const auto token = bearerToken(request);
|
||||
if (!token)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto db = drogon::app().getDbClient();
|
||||
const auto result = db->execSqlSync(
|
||||
"SELECT u.id, u.email, COALESCE(MIN(r.name), 'citizen') AS role "
|
||||
"FROM api_tokens t "
|
||||
"JOIN users u ON u.id = t.user_id "
|
||||
"LEFT JOIN user_roles ur ON ur.user_id = u.id "
|
||||
"LEFT JOIN roles r ON r.id = ur.role_id "
|
||||
"WHERE t.token_hash = ? AND t.revoked_at IS NULL "
|
||||
"AND t.expires_at > NOW() AND u.banned = FALSE "
|
||||
"GROUP BY u.id, u.email",
|
||||
crypto::hashSecret(*token));
|
||||
|
||||
if (result.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto &row = result[0];
|
||||
return UserContext{row["id"].as<long long>(),
|
||||
row["email"].as<std::string>(),
|
||||
row["role"].as<std::string>()};
|
||||
}
|
||||
|
||||
bool hasRole(const UserContext &user, const std::string &role)
|
||||
{
|
||||
return user.role == role;
|
||||
}
|
||||
|
||||
bool canManageReports(const UserContext &user)
|
||||
{
|
||||
return user.role == "admin" || user.role == "operator";
|
||||
}
|
||||
|
||||
bool isAdmin(const UserContext &user)
|
||||
{
|
||||
return user.role == "admin";
|
||||
}
|
||||
} // namespace visione::auth
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <drogon/HttpRequest.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace visione::auth
|
||||
{
|
||||
struct UserContext
|
||||
{
|
||||
long long id{0};
|
||||
std::string email;
|
||||
std::string role;
|
||||
};
|
||||
|
||||
std::optional<std::string> bearerToken(const drogon::HttpRequestPtr &request);
|
||||
std::optional<UserContext> authenticate(const drogon::HttpRequestPtr &request);
|
||||
bool hasRole(const UserContext &user, const std::string &role);
|
||||
bool canManageReports(const UserContext &user);
|
||||
bool isAdmin(const UserContext &user);
|
||||
} // namespace visione::auth
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "common/Crypto.h"
|
||||
|
||||
#include <drogon/utils/Utilities.h>
|
||||
|
||||
#include <iomanip>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace visione::crypto
|
||||
{
|
||||
std::string sha256(const std::string &value)
|
||||
{
|
||||
return drogon::utils::getSha256(value);
|
||||
}
|
||||
|
||||
std::string randomNumericCode(int digits)
|
||||
{
|
||||
std::random_device device;
|
||||
std::mt19937 generator(device());
|
||||
std::uniform_int_distribution<int> distribution(0, 9);
|
||||
|
||||
std::string code;
|
||||
code.reserve(static_cast<size_t>(digits));
|
||||
for (int i = 0; i < digits; ++i)
|
||||
{
|
||||
code.push_back(static_cast<char>('0' + distribution(generator)));
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
std::string randomToken()
|
||||
{
|
||||
return drogon::utils::getUuid() + drogon::utils::getUuid();
|
||||
}
|
||||
|
||||
std::string hashPassword(const std::string &password)
|
||||
{
|
||||
const auto salt = drogon::utils::getUuid();
|
||||
return "sha256$" + salt + "$" + sha256(salt + ":" + password);
|
||||
}
|
||||
|
||||
bool verifyPassword(const std::string &password, const std::string &hash)
|
||||
{
|
||||
constexpr std::string_view prefix = "sha256$";
|
||||
if (!hash.starts_with(prefix))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto saltStart = prefix.size();
|
||||
const auto hashStart = hash.find('$', saltStart);
|
||||
if (hashStart == std::string::npos)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto salt = hash.substr(saltStart, hashStart - saltStart);
|
||||
const auto expected = "sha256$" + salt + "$" + sha256(salt + ":" + password);
|
||||
return expected == hash;
|
||||
}
|
||||
|
||||
std::string hashSecret(const std::string &secret)
|
||||
{
|
||||
return sha256("visione:" + secret);
|
||||
}
|
||||
} // namespace visione::crypto
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace visione::crypto
|
||||
{
|
||||
std::string sha256(const std::string &value);
|
||||
std::string randomNumericCode(int digits);
|
||||
std::string randomToken();
|
||||
std::string hashPassword(const std::string &password);
|
||||
bool verifyPassword(const std::string &password, const std::string &hash);
|
||||
std::string hashSecret(const std::string &secret);
|
||||
} // namespace visione::crypto
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
#include "common/Email.h"
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include <netdb.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace visione::email
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string configString(const std::string &key, const std::string &fallback)
|
||||
{
|
||||
const auto &mail = drogon::app().getCustomConfig()["mail"];
|
||||
return mail.get(key, fallback).asString();
|
||||
}
|
||||
|
||||
int configInt(const std::string &key, int fallback)
|
||||
{
|
||||
const auto &mail = drogon::app().getCustomConfig()["mail"];
|
||||
return mail.get(key, fallback).asInt();
|
||||
}
|
||||
|
||||
bool readResponse(int socket)
|
||||
{
|
||||
std::array<char, 512> buffer{};
|
||||
const auto count = recv(socket, buffer.data(), buffer.size() - 1, 0);
|
||||
return count > 0 && buffer[0] >= '2' && buffer[0] <= '3';
|
||||
}
|
||||
|
||||
bool sendLine(int socket, const std::string &line)
|
||||
{
|
||||
const auto payload = line + "\r\n";
|
||||
return send(socket, payload.data(), payload.size(), 0) ==
|
||||
static_cast<ssize_t>(payload.size());
|
||||
}
|
||||
|
||||
bool sendCommand(int socket, const std::string &line)
|
||||
{
|
||||
return sendLine(socket, line) && readResponse(socket);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool EmailService::sendOtp(const std::string &to, const std::string &code) const
|
||||
{
|
||||
const auto host = configString("smtp_host", "127.0.0.1");
|
||||
const auto port = std::to_string(configInt("smtp_port", 1025));
|
||||
const auto from = configString("from", "[email protected]");
|
||||
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
addrinfo *addresses = nullptr;
|
||||
if (getaddrinfo(host.c_str(), port.c_str(), &hints, &addresses) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int smtp = -1;
|
||||
for (auto *address = addresses; address != nullptr; address = address->ai_next)
|
||||
{
|
||||
smtp = socket(address->ai_family, address->ai_socktype, address->ai_protocol);
|
||||
if (smtp < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (connect(smtp, address->ai_addr, address->ai_addrlen) == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
close(smtp);
|
||||
smtp = -1;
|
||||
}
|
||||
freeaddrinfo(addresses);
|
||||
|
||||
if (smtp < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto closeSocket = [&smtp]() {
|
||||
if (smtp >= 0)
|
||||
{
|
||||
close(smtp);
|
||||
smtp = -1;
|
||||
}
|
||||
};
|
||||
|
||||
if (!readResponse(smtp) || !sendCommand(smtp, "HELO visione.local") ||
|
||||
!sendCommand(smtp, "MAIL FROM:<" + from + ">") ||
|
||||
!sendCommand(smtp, "RCPT TO:<" + to + ">") ||
|
||||
!sendCommand(smtp, "DATA"))
|
||||
{
|
||||
closeSocket();
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string body =
|
||||
"From: Visione <" + from + ">\r\n"
|
||||
"To: " + to + "\r\n"
|
||||
"Subject: Codice OTP Visione\r\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\r\n"
|
||||
"\r\n"
|
||||
"Il tuo codice OTP per Visione e': " + code + "\r\n"
|
||||
"Scade tra 10 minuti.\r\n"
|
||||
".";
|
||||
|
||||
const auto ok = sendCommand(smtp, body) && sendCommand(smtp, "QUIT");
|
||||
closeSocket();
|
||||
return ok;
|
||||
}
|
||||
} // namespace visione::email
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace visione::email
|
||||
{
|
||||
class EmailService
|
||||
{
|
||||
public:
|
||||
bool sendOtp(const std::string &to, const std::string &code) const;
|
||||
};
|
||||
} // namespace visione::email
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "common/GeoJson.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace visione::geojson
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct Point
|
||||
{
|
||||
double x;
|
||||
double y;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
bool pointInPolygon(double latitude, double longitude, const Json::Value &geojson)
|
||||
{
|
||||
if (!geojson.isObject() || geojson["type"].asString() != "Polygon" ||
|
||||
!geojson["coordinates"].isArray() || geojson["coordinates"].empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto &ring = geojson["coordinates"][0];
|
||||
if (!ring.isArray() || ring.size() < 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<Point> points;
|
||||
points.reserve(ring.size());
|
||||
for (const auto &coordinate : ring)
|
||||
{
|
||||
if (!coordinate.isArray() || coordinate.size() < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
points.push_back({coordinate[0].asDouble(), coordinate[1].asDouble()});
|
||||
}
|
||||
|
||||
const auto x = longitude;
|
||||
const auto y = latitude;
|
||||
bool inside = false;
|
||||
for (size_t i = 0, j = points.size() - 1; i < points.size(); j = i++)
|
||||
{
|
||||
const auto &pi = points[i];
|
||||
const auto &pj = points[j];
|
||||
const bool intersects =
|
||||
((pi.y > y) != (pj.y > y)) &&
|
||||
(x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x);
|
||||
if (intersects)
|
||||
{
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
} // namespace visione::geojson
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
namespace visione::geojson
|
||||
{
|
||||
bool pointInPolygon(double latitude, double longitude, const Json::Value &geojson);
|
||||
} // namespace visione::geojson
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "common/Http.h"
|
||||
|
||||
namespace visione::http
|
||||
{
|
||||
drogon::HttpResponsePtr json(Json::Value body, drogon::HttpStatusCode status)
|
||||
{
|
||||
auto response = drogon::HttpResponse::newHttpJsonResponse(std::move(body));
|
||||
response->setStatusCode(status);
|
||||
return response;
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr error(const std::string &code,
|
||||
drogon::HttpStatusCode status,
|
||||
const std::string &message)
|
||||
{
|
||||
Json::Value body;
|
||||
body["error"] = code;
|
||||
if (!message.empty())
|
||||
{
|
||||
body["message"] = message;
|
||||
}
|
||||
return json(std::move(body), status);
|
||||
}
|
||||
|
||||
Json::Value requireJson(const drogon::HttpRequestPtr &request)
|
||||
{
|
||||
const auto &body = request->getJsonObject();
|
||||
if (!body)
|
||||
{
|
||||
return Json::Value(Json::nullValue);
|
||||
}
|
||||
return *body;
|
||||
}
|
||||
} // namespace visione::http
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <drogon/HttpResponse.h>
|
||||
|
||||
#include <json/json.h>
|
||||
#include <string>
|
||||
|
||||
namespace visione::http
|
||||
{
|
||||
drogon::HttpResponsePtr json(Json::Value body,
|
||||
drogon::HttpStatusCode status = drogon::k200OK);
|
||||
drogon::HttpResponsePtr error(const std::string &code,
|
||||
drogon::HttpStatusCode status,
|
||||
const std::string &message = {});
|
||||
Json::Value requireJson(const drogon::HttpRequestPtr &request);
|
||||
} // namespace visione::http
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "common/Validation.h"
|
||||
|
||||
#include <regex>
|
||||
|
||||
namespace visione::validation
|
||||
{
|
||||
bool hasString(const Json::Value &body, const std::string &key)
|
||||
{
|
||||
return body.isMember(key) && body[key].isString() &&
|
||||
!body[key].asString().empty();
|
||||
}
|
||||
|
||||
bool hasNumber(const Json::Value &body, const std::string &key)
|
||||
{
|
||||
return body.isMember(key) && body[key].isNumeric();
|
||||
}
|
||||
|
||||
std::vector<std::string> missingStrings(
|
||||
const Json::Value &body,
|
||||
std::initializer_list<const char *> keys)
|
||||
{
|
||||
std::vector<std::string> missing;
|
||||
for (const auto *key : keys)
|
||||
{
|
||||
if (!hasString(body, key))
|
||||
{
|
||||
missing.emplace_back(key);
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
bool isValidEmail(const std::string &email)
|
||||
{
|
||||
static const std::regex pattern(R"(^[^@\s]+@[^@\s]+\.[^@\s]+$)");
|
||||
return std::regex_match(email, pattern);
|
||||
}
|
||||
|
||||
bool isValidFiscalCode(const std::string &fiscalCode)
|
||||
{
|
||||
static const std::regex pattern(R"(^[A-Za-z0-9]{16}$)");
|
||||
return std::regex_match(fiscalCode, pattern);
|
||||
}
|
||||
} // namespace visione::validation
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <json/json.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace visione::validation
|
||||
{
|
||||
bool hasString(const Json::Value &body, const std::string &key);
|
||||
bool hasNumber(const Json::Value &body, const std::string &key);
|
||||
std::vector<std::string> missingStrings(const Json::Value &body,
|
||||
std::initializer_list<const char *> keys);
|
||||
bool isValidEmail(const std::string &email);
|
||||
bool isValidFiscalCode(const std::string &fiscalCode);
|
||||
} // namespace visione::validation
|
||||
|
||||
Reference in New Issue
Block a user