Add Visione backend foundation

This commit is contained in:
gabdevele
2026-06-02 19:22:00 +02:00 Unverified
commit 580789ac83
52 changed files with 5008 additions and 0 deletions
+66
View File
@@ -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
+23
View File
@@ -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
+68
View File
@@ -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
+14
View File
@@ -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
View File
@@ -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
+13
View File
@@ -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
+59
View File
@@ -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
+9
View File
@@ -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
+35
View File
@@ -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
+17
View File
@@ -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
+45
View File
@@ -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
+16
View File
@@ -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