Compare commits

..
2 Commits
29 changed files with 1196 additions and 384 deletions
-12
View File
@@ -5,16 +5,4 @@ MYSQL_PASSWORD=visione
MYSQL_ROOT_PASSWORD=root MYSQL_ROOT_PASSWORD=root
MAILPIT_SMTP_PORT=1025 MAILPIT_SMTP_PORT=1025
MAILPIT_WEB_PORT=8025 MAILPIT_WEB_PORT=8025
MAIL_PROVIDER=smtp
SMTP_HOST=127.0.0.1
SMTP_PORT=1025
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_USE_TLS=false
SMTP_USE_STARTTLS=false
SMTP_VERIFY_PEER=false
SMTP_VERIFY_HOST=false
MAIL_FROM=[email protected]
CORS_ALLOWED_ORIGIN=http://localhost:3000 CORS_ALLOWED_ORIGIN=http://localhost:3000
+41
View File
@@ -0,0 +1,41 @@
# Visione Backend Agent Guide
## Project Context
- This repository is the backend for **Visione**, an urban issue reporting webapp.
- The backend is C++20 with Drogon. Keep code, symbols, API fields, database names and comments in English.
- User-facing product labels may be Italian when they represent UI/business vocabulary.
- Docker is for local external services only: MySQL, Mailpit and future Redis-like dependencies.
ENVIRONMENT:
You are running in a docker container, so it's normal if something doesn't work as expected.
You are allowed to install new tools, libraries or dependencies if you think they are necessary to complete your task.
Since you are running in a docker container, you could have some build issues sometimes, do not waste tokens over-trying to fix them, just report the issue and move on to the next task if you believe the issue is not related to your task.
Prefer using commands to read files, search for patterns ecc... since I have installed a tool that helps reduce token usage for those operations.
## Authoritative References
- Local Drogon source: `/home/gabdevele/Projects/snitchitaly/backend/drogon`
- Official Drogon docs: `https://drogonframework.github.io/drogon-docs/`
- Project docs start from `docs/LLM_CONTEXT.md`.
## Architecture Rules
- Controllers parse HTTP input and shape HTTP responses only.
- Services own business rules.
- Repositories/database helpers own SQL access.
- Prefer small, readable C++ over clever abstractions.
- Use parameterized SQL; do not concatenate user input into SQL.
- Keep local development simple. Do not introduce production infrastructure unless requested.
## Commands
- Configure: `cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug`
- Build: `cmake --build build`
- Test: `ctest --test-dir build --output-on-failure`
- OpenAPI: `cmake --build build --target openapi`
- Lab frontend: `cd scripts/lab && uv run streamlit run app.py`
- Local services: `docker compose up -d`
- App: `./build/visione-backend`
## Development Workflow
- Update docs when behavior, schema, setup or API contracts change.
- After each completed and verified feature, create a short commit.
- Keep commit messages concise, for example `Add Mailpit config` or `Add report API`.
- Do not spend excessive time on exhaustive tests; cover high-risk behavior and regressions.
-2
View File
@@ -9,7 +9,6 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(Python3 COMPONENTS Interpreter REQUIRED) find_package(Python3 COMPONENTS Interpreter REQUIRED)
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
find_package(CURL REQUIRED)
option(USE_SYSTEM_DROGON "Use an installed Drogon package instead of CPM" OFF) option(USE_SYSTEM_DROGON "Use an installed Drogon package instead of CPM" OFF)
@@ -73,7 +72,6 @@ target_include_directories(${PROJECT_NAME}
target_link_libraries(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME}
PRIVATE PRIVATE
Drogon::Drogon Drogon::Drogon
CURL::libcurl
OpenSSL::Crypto OpenSSL::Crypto
) )
+2 -2
View File
@@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential ccache cmake ninja-build git pkg-config \ build-essential ccache cmake ninja-build git pkg-config \
libcurl4-openssl-dev libssl-dev zlib1g-dev \ libssl-dev zlib1g-dev \
libmariadb-dev libmariadb-dev-compat \ libmariadb-dev libmariadb-dev-compat \
libsqlite3-dev python3 libjsoncpp-dev uuid-dev ca-certificates \ libsqlite3-dev python3 libjsoncpp-dev uuid-dev ca-certificates \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@@ -27,7 +27,7 @@ FROM ubuntu:22.04 AS runtime
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
libcurl4 libssl3 ca-certificates libstdc++6 libmariadb3 libjsoncpp25 libuuid1 \ libssl3 ca-certificates libstdc++6 libmariadb3 libjsoncpp25 libuuid1 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
+17 -40
View File
@@ -9,7 +9,7 @@ Install native build dependencies first. On Ubuntu:
```bash ```bash
sudo apt install build-essential cmake ninja-build git pkg-config \ sudo apt install build-essential cmake ninja-build git pkg-config \
libcurl4-openssl-dev libjsoncpp-dev uuid-dev zlib1g-dev libssl-dev \ libjsoncpp-dev uuid-dev zlib1g-dev libssl-dev \
libmariadb-dev libmariadb-dev-compat libmariadb-dev libmariadb-dev-compat
``` ```
@@ -36,8 +36,8 @@ has been built.
Local seeded admin: Local seeded admin:
- email: `admin@visionebari.it` - email: `admin@visione.gov.it`
- access uses OTP email delivery and needs mail provider config. - access uses OTP through Mailpit and is for local development only.
If you already had a MySQL volume from the old scaffold, create the local If you already had a MySQL volume from the old scaffold, create the local
database/user once: database/user once:
@@ -56,13 +56,24 @@ curl http://localhost:5555/api/v1/health
curl http://localhost:5555/api/v1/ready curl http://localhost:5555/api/v1/ready
curl -X POST http://localhost:5555/api/v1/auth/otp/request \ curl -X POST http://localhost:5555/api/v1/auth/otp/request \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{"email":"admin@visionebari.it"}' -d '{"email":"admin@visione.gov.it"}'
``` ```
Use `-DUSE_SYSTEM_DROGON=ON` only if Drogon is already installed and Use `-DUSE_SYSTEM_DROGON=ON` only if Drogon is already installed and
discoverable by CMake. The default build uses CPM.cmake and downloads Drogon discoverable by CMake. The default build uses CPM.cmake and downloads Drogon
`v1.9.13`. `v1.9.13`.
## Project Docs
- `AGENTS.md`: durable instructions for coding agents.
- `docs/LLM_CONTEXT.md`: short project context for future LLM sessions.
- `docs/MILESTONE_01.md`: what this milestone implemented and what comes next.
- `docs/MILESTONE_02.md`: workflow and authorization milestone.
- `docs/MILESTONE_03.md`: media upload, OpenAPI generator and API lab milestone.
- `docs/MILESTONE_04.md`: backend completion milestone.
- `docs/API_SPEC.md`: API surface.
- `docs/DATABASE_SCHEMA.md`: schema overview.
- `docs/ARCHITECTURE.md`: backend structure.
## OpenAPI ## OpenAPI
@@ -82,32 +93,6 @@ python3 -m unittest scripts/test_generate_openapi.py
The app serves the generated JSON at `/api/v1/openapi.json` and Swagger UI at The app serves the generated JSON at `/api/v1/openapi.json` and Swagger UI at
`/api/v1/docs`. `/api/v1/docs`.
## Email Delivery
OTP delivery uses SMTP through `libcurl`. Local dev defaults to Mailpit.
Set these environment variables before starting backend for local Mailpit:
```bash
export MAIL_PROVIDER=smtp
export SMTP_HOST=127.0.0.1
export SMTP_PORT=1025
export MAIL_FROM=[email protected]
```
For Resend in production, use SMTP credentials instead:
```bash
export MAIL_PROVIDER=resend
export SMTP_HOST=smtp.resend.com
export SMTP_PORT=465
export SMTP_USERNAME=resend
export SMTP_PASSWORD=re_xxx
export SMTP_USE_TLS=true
export SMTP_VERIFY_PEER=true
export SMTP_VERIFY_HOST=true
export MAIL_FROM=[email protected]
```
## Visione Lab ## Visione Lab
`scripts/lab/` is a small Streamlit frontend used only to test the backend `scripts/lab/` is a small Streamlit frontend used only to test the backend
@@ -152,23 +137,15 @@ backend deployment with MySQL and a local SMTP catcher. Configure the public
domain in Dokploy against service `backend` and container port `5555`. domain in Dokploy against service `backend` and container port `5555`.
The runtime image includes `config.json`; override environment-specific values The runtime image includes `config.json`; override environment-specific values
with Dokploy environment variables. The Mailpit service uses the with Dokploy environment variables. The SMTP catcher service uses the
multi-architecture Mailpit image to avoid `exec format error` failures on ARM multi-architecture Mailpit image to avoid `exec format error` failures on ARM
servers. servers.
Set `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `MYSQL_ROOT_PASSWORD`, Set `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `MYSQL_ROOT_PASSWORD` and
`SMTP_PASSWORD`, `MAIL_FROM` and
`CORS_ALLOWED_ORIGIN` in Dokploy environment variables. Use the exact browser `CORS_ALLOWED_ORIGIN` in Dokploy environment variables. Use the exact browser
origin without quotes or a trailing slash, for example origin without quotes or a trailing slash, for example
`https://visione.studentindipendenti.it`. `https://visione.studentindipendenti.it`.
For CIE auth deploys, also override values that vary by environment:
- `CIE_AUTH_SERVICE_BASE_URL`
- `CIE_AUTH_START_PATH`
- `CIE_AUTH_SHARED_SECRET`
- `CIE_AUTH_STATE_TTL_MINUTES`
- `CIE_AUTH_DEFAULT_FRONTEND_REDIRECT_URI`
Docker builds use BuildKit cache mounts for CPM dependencies and `ccache`, so Docker builds use BuildKit cache mounts for CPM dependencies and `ccache`, so
repeat Dokploy builds can reuse Drogon and C++ compilation cache. repeat Dokploy builds can reuse Drogon and C++ compilation cache.
+69 -189
View File
@@ -1,239 +1,119 @@
#include "common/Email.h" #include "common/Email.h"
#include <curl/curl.h>
#include <drogon/drogon.h> #include <drogon/drogon.h>
#include <algorithm> #include <array>
#include <cstddef>
#include <cstring> #include <cstring>
#include <memory> #include <string>
#include <sstream>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
namespace visione::email namespace visione::email
{ {
namespace namespace
{ {
const Json::Value &mailConfig()
{
return drogon::app().getCustomConfig()["mail"];
}
std::string configString(const std::string &key, const std::string &fallback) std::string configString(const std::string &key, const std::string &fallback)
{ {
const auto &mail = mailConfig(); const auto &mail = drogon::app().getCustomConfig()["mail"];
return mail.get(key, fallback).asString(); return mail.get(key, fallback).asString();
} }
int configInt(const std::string &key, int fallback) int configInt(const std::string &key, int fallback)
{ {
const auto &mail = mailConfig(); const auto &mail = drogon::app().getCustomConfig()["mail"];
return mail.get(key, fallback).asInt(); return mail.get(key, fallback).asInt();
} }
bool configBool(const std::string &key, bool fallback) bool readResponse(int socket)
{ {
const auto &mail = mailConfig(); std::array<char, 512> buffer{};
return mail.get(key, fallback).asBool(); const auto count = recv(socket, buffer.data(), buffer.size() - 1, 0);
return count > 0 && buffer[0] >= '2' && buffer[0] <= '3';
} }
struct SmtpDefaults bool sendLine(int socket, const std::string &line)
{ {
std::string host; const auto payload = line + "\r\n";
int port; return send(socket, payload.data(), payload.size(), 0) ==
std::string username; static_cast<ssize_t>(payload.size());
bool useTls;
bool useStartTls;
bool verifyPeer;
bool verifyHost;
};
SmtpDefaults smtpDefaults(const std::string &provider)
{
if (provider == "resend")
{
return {
.host = "smtp.resend.com",
.port = 465,
.username = "resend",
.useTls = true,
.useStartTls = false,
.verifyPeer = true,
.verifyHost = true,
};
} }
return { bool sendCommand(int socket, const std::string &line)
.host = "127.0.0.1",
.port = 1025,
.username = "",
.useTls = false,
.useStartTls = false,
.verifyPeer = false,
.verifyHost = false,
};
}
std::string smtpUrl(const std::string &host, int port, bool useTls)
{ {
return (useTls ? "smtps://" : "smtp://") + host + ":" + std::to_string(port); return sendLine(socket, line) && readResponse(socket);
}
struct UploadContext
{
const std::string *payload;
size_t offset{0};
};
size_t readPayload(char *buffer, size_t size, size_t count, void *userData)
{
auto *context = static_cast<UploadContext *>(userData);
const auto capacity = size * count;
const auto remaining = context->payload->size() - context->offset;
const auto bytesToCopy = std::min(capacity, remaining);
if (bytesToCopy == 0)
{
return 0;
}
std::memcpy(buffer,
context->payload->data() + static_cast<std::ptrdiff_t>(context->offset),
bytesToCopy);
context->offset += bytesToCopy;
return bytesToCopy;
}
bool ensureCurlInitialized()
{
static const bool initialized = []() {
const auto result = curl_global_init(CURL_GLOBAL_DEFAULT);
if (result != CURLE_OK)
{
LOG_ERROR << "curl_global_init failed: " << curl_easy_strerror(result);
return false;
}
return true;
}();
return initialized;
} }
} // namespace } // namespace
std::string EmailService::deliveryName() const
{
return configString("provider", "smtp");
}
bool EmailService::sendOtp(const std::string &to, const std::string &code) const bool EmailService::sendOtp(const std::string &to, const std::string &code) const
{ {
const auto provider = deliveryName(); const auto host = configString("smtp_host", "127.0.0.1");
const auto defaults = smtpDefaults(provider); const auto port = std::to_string(configInt("smtp_port", 1025));
const auto host = configString("host", defaults.host); const auto from = configString("from", "[email protected]");
const auto port = configInt("port", defaults.port);
const auto username = configString("username", defaults.username);
const auto password = configString("password", "");
const auto from = configString("from", "");
const auto useTls = configBool("use_tls", defaults.useTls);
const auto useStartTls = configBool("use_starttls", defaults.useStartTls);
const auto verifyPeer = configBool("verify_peer", defaults.verifyPeer);
const auto verifyHost = configBool("verify_host", defaults.verifyHost);
if (host.empty() || from.empty()) addrinfo hints{};
{ hints.ai_family = AF_UNSPEC;
LOG_ERROR << "Email delivery configuration missing: host or from"; hints.ai_socktype = SOCK_STREAM;
return false;
}
if (provider == "resend" && password.empty())
{
LOG_ERROR << "Email delivery configuration missing: SMTP password for Resend";
return false;
}
if (!ensureCurlInitialized()) addrinfo *addresses = nullptr;
if (getaddrinfo(host.c_str(), port.c_str(), &hints, &addresses) != 0)
{ {
return false; return false;
} }
std::ostringstream payload; int smtp = -1;
payload << "To: <" << to << ">\r\n"; for (auto *address = addresses; address != nullptr; address = address->ai_next)
payload << "From: Visione <" << from << ">\r\n"; {
payload << "Subject: Codice OTP Visione\r\n"; smtp = socket(address->ai_family, address->ai_socktype, address->ai_protocol);
payload << "MIME-Version: 1.0\r\n"; if (smtp < 0)
payload << "Content-Type: text/plain; charset=utf-8\r\n"; {
payload << "\r\n"; continue;
payload << "Il tuo codice OTP per Visione e': " << code << "\r\n"; }
payload << "Scade tra 10 minuti.\r\n"; if (connect(smtp, address->ai_addr, address->ai_addrlen) == 0)
const auto payloadString = payload.str(); {
const auto url = smtpUrl(host, port, useTls); break;
const auto mailFrom = "<" + from + ">"; }
const auto recipientAddress = "<" + to + ">"; close(smtp);
smtp = -1;
}
freeaddrinfo(addresses);
using CurlHandle = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>; if (smtp < 0)
using CurlList = std::unique_ptr<curl_slist, decltype(&curl_slist_free_all)>;
CurlHandle curl(curl_easy_init(), &curl_easy_cleanup);
if (!curl)
{ {
LOG_ERROR << "Could not initialize libcurl SMTP handle";
return false; return false;
} }
CurlList recipients(nullptr, &curl_slist_free_all); const auto closeSocket = [&smtp]() {
recipients.reset(curl_slist_append(nullptr, recipientAddress.c_str())); if (smtp >= 0)
if (!recipients)
{ {
LOG_ERROR << "Could not build SMTP recipient list for " << to; 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; return false;
} }
UploadContext uploadContext{.payload = &payloadString}; const std::string body =
char errorBuffer[CURL_ERROR_SIZE] = {0}; "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"
".";
curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); const auto ok = sendCommand(smtp, body) && sendCommand(smtp, "QUIT");
curl_easy_setopt(curl.get(), CURLOPT_MAIL_FROM, mailFrom.c_str()); closeSocket();
curl_easy_setopt(curl.get(), CURLOPT_MAIL_RCPT, recipients.get()); return ok;
curl_easy_setopt(curl.get(), CURLOPT_READFUNCTION, readPayload);
curl_easy_setopt(curl.get(), CURLOPT_READDATA, &uploadContext);
curl_easy_setopt(curl.get(), CURLOPT_UPLOAD, 1L);
curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, "visione-backend/1.0");
curl_easy_setopt(curl.get(), CURLOPT_CONNECTTIMEOUT, 10L);
curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 20L);
curl_easy_setopt(curl.get(), CURLOPT_ERRORBUFFER, errorBuffer);
curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, verifyPeer ? 1L : 0L);
curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, verifyHost ? 2L : 0L);
if (!username.empty())
{
curl_easy_setopt(curl.get(), CURLOPT_USERNAME, username.c_str());
}
if (!password.empty())
{
curl_easy_setopt(curl.get(), CURLOPT_PASSWORD, password.c_str());
}
if (useStartTls)
{
curl_easy_setopt(curl.get(), CURLOPT_USE_SSL, CURLUSESSL_ALL);
}
const auto result = curl_easy_perform(curl.get());
if (result != CURLE_OK)
{
LOG_ERROR << "OTP email delivery failed via SMTP for "
<< to
<< ", provider="
<< provider
<< ", host="
<< host
<< ", port="
<< port
<< ", error="
<< (errorBuffer[0] != '\0' ? errorBuffer : curl_easy_strerror(result));
return false;
}
LOG_INFO << "OTP email sent via SMTP for " << to
<< ", provider=" << provider
<< ", host=" << host
<< ", port=" << port;
return true;
} }
} // namespace visione::email } // namespace visione::email
+1 -1
View File
@@ -8,6 +8,6 @@ class EmailService
{ {
public: public:
bool sendOtp(const std::string &to, const std::string &code) const; bool sendOtp(const std::string &to, const std::string &code) const;
std::string deliveryName() const;
}; };
} // namespace visione::email } // namespace visione::email
+4 -11
View File
@@ -64,16 +64,9 @@
"default_frontend_redirect_uri": "http://localhost:3000/auth/cie/callback" "default_frontend_redirect_uri": "http://localhost:3000/auth/cie/callback"
}, },
"mail": { "mail": {
"provider": "smtp", "smtp_host": "127.0.0.1",
"host": "127.0.0.1", "smtp_port": 1025,
"port": 1025, "from": "[email protected]"
"username": "",
"password": "",
"from": "[email protected]",
"use_tls": false,
"use_starttls": false,
"verify_peer": false,
"verify_host": false
}, },
"uploads": { "uploads": {
"public_base_path": "/uploads", "public_base_path": "/uploads",
@@ -83,7 +76,7 @@
"spec_path": "build/openapi/visione.openapi.json" "spec_path": "build/openapi/visione.openapi.json"
}, },
"cors": { "cors": {
"allowed_origin": "http://127.0.0.1:3000" "allowed_origin": "http://localhost:3000"
} }
} }
} }
+1 -2
View File
@@ -57,8 +57,7 @@ void sendService(Json::Value body,
else if (error == "invalid_status_transition" || else if (error == "invalid_status_transition" ||
error == "media_limit_reached" || error == "media_limit_reached" ||
error == "report_not_open" || error == "report_not_open" ||
error == "report_owned_by_other_operator" || error == "report_owned_by_other_operator")
error == "feedback_already_exists")
{ {
status = drogon::k409Conflict; status = drogon::k409Conflict;
} }
+14 -26
View File
@@ -13,25 +13,14 @@ services:
DB_PASSWORD: ${DB_PASSWORD:-visione} DB_PASSWORD: ${DB_PASSWORD:-visione}
DB_NAME: ${DB_NAME:-visione} DB_NAME: ${DB_NAME:-visione}
DB_TIMEOUT: ${DB_TIMEOUT:-30} DB_TIMEOUT: ${DB_TIMEOUT:-30}
MAIL_PROVIDER: ${MAIL_PROVIDER:-resend} SMTP_HOST: ${SMTP_HOST:-mailpit}
SMTP_HOST: ${SMTP_HOST:-smtp.resend.com} SMTP_PORT: ${SMTP_PORT:-1025}
SMTP_PORT: ${SMTP_PORT:-465}
SMTP_USERNAME: ${SMTP_USERNAME:-resend}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
SMTP_USE_STARTTLS: ${SMTP_USE_STARTTLS:-false}
SMTP_VERIFY_PEER: ${SMTP_VERIFY_PEER:-true}
SMTP_VERIFY_HOST: ${SMTP_VERIFY_HOST:-true}
RESEND_API_KEY: ${RESEND_API_KEY:-}
MAIL_FROM: ${MAIL_FROM:[email protected]}
CORS_ALLOWED_ORIGIN: ${CORS_ALLOWED_ORIGIN:-http://localhost:3000} CORS_ALLOWED_ORIGIN: ${CORS_ALLOWED_ORIGIN:-http://localhost:3000}
CIE_AUTH_SERVICE_BASE_URL: ${CIE_AUTH_SERVICE_BASE_URL:-http://127.0.0.1:8001}
CIE_AUTH_START_PATH: ${CIE_AUTH_START_PATH:-/visione/start}
CIE_AUTH_SHARED_SECRET: ${CIE_AUTH_SHARED_SECRET:-change-me-for-local-dev}
CIE_AUTH_STATE_TTL_MINUTES: ${CIE_AUTH_STATE_TTL_MINUTES:-10}
CIE_AUTH_DEFAULT_FRONTEND_REDIRECT_URI: ${CIE_AUTH_DEFAULT_FRONTEND_REDIRECT_URI:-http://localhost:3000/auth/cie/callback}
expose: expose:
- "5555" - "5555"
networks:
- visione
- dokploy-network
depends_on: depends_on:
mysql: mysql:
condition: service_healthy condition: service_healthy
@@ -48,6 +37,8 @@ services:
- "3306" - "3306"
volumes: volumes:
- mysql-data:/var/lib/mysql - mysql-data:/var/lib/mysql
networks:
- visione
healthcheck: healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u$${MYSQL_USER} -p$${MYSQL_PASSWORD} --silent"] test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u$${MYSQL_USER} -p$${MYSQL_PASSWORD} --silent"]
interval: 5s interval: 5s
@@ -61,17 +52,14 @@ services:
expose: expose:
- "1025" - "1025"
- "8025" - "8025"
networks:
phpmyadmin: - visione
image: phpmyadmin:latest
restart: unless-stopped
expose:
- "80"
environment:
PMA_HOST: mysql
PMA_USER: ${DB_USER:-visione}
PMA_PASSWORD: ${DB_PASSWORD:-visione}
volumes: volumes:
mysql-data: mysql-data:
uploads-data: uploads-data:
networks:
visione:
dokploy-network:
external: true
+164
View File
@@ -0,0 +1,164 @@
# Visione API Spec
Base path: `/api/v1`
## Auth Header
```http
Authorization: Bearer <token>
```
## Roles
- public: no auth
- authenticated: any valid user
- operator: `reports.manage` or admin
- admin: role/permission admin
## HTTP Semantics
- `200`: success
- `401`: missing or invalid auth
- `403`: authenticated but forbidden
- `404`: missing resource or invalid public intervention token
- `409`: workflow or ownership conflict
- `422`: semantic payload/reference error
## Technical
- `GET /health`
- `GET /ready`
- `GET /openapi.json`
- `GET /docs`
## Auth + Profile
- `POST /auth/register`
citizen registration without password
- `POST /auth/otp/request`
sends OTP through Mailpit
- `POST /auth/otp/verify`
consumes OTP, returns bearer token
- `POST /auth/cie/start`
creates local CIE login state and returns Django RP authorization URL
- `GET /auth/cie/session/{state}`
frontend polling endpoint; returns final Visione bearer token on success
- `GET /me`
current user profile, roles, permissions, points
- `PATCH /me`
editable fields today: `first_name`, `last_name`, `phone`
## Public Reference / Public Access
- `GET /areas`
municipal areas
- `GET /interventions/{token}`
public external-team summary link
- `GET /open-data/reports`
anonymized reports in `RESOLVED` or `CLOSED`
filtered to Bari municipal boundary
## User Reference / Personal
- `GET /report-categories`
active report categories, authenticated users only
- `GET /notifications`
- `PATCH /notifications/{id}/read`
- `PATCH /notifications/read-all`
- `GET /me/rewards`
## Report Endpoints
- `POST /reports`
create citizen report, optional inline `media`, area inferred by GeoJSON
coordinates must stay inside allowed Bari municipal boundary
- `GET /reports`
citizen sees own reports; operator/admin see all
- `GET /reports/{id}`
detail + status history + media
- `GET /reports/{id}/messages`
- `POST /reports/{id}/messages`
- `POST /reports/{id}/media`
max 5 total media, image/video only
- `POST /reports/{id}/feedback`
only owner, only when status is `RESOLVED`
## Operator Workflow
- `GET /operator/teams`
active external intervention teams
- `GET /operator/reports/open`
- `GET /operator/reports/assigned`
- `GET /operator/reports/others`
- `POST /reports/{id}/take`
`OPEN -> UNDER_REVIEW`
- `PATCH /reports/{id}/status`
only owner operator or admin, valid transitions only
- `POST /reports/{id}/assignments`
assign external team
if report is `OPEN`, assignment can directly claim report and move to `ASSIGNED`
- `GET /reports/{id}/intervention-summary`
operator/admin full summary with citizen contact data
- `GET /reports/{id}/team-suggestions`
suggestions by category/team-category mapping and area
- `POST /reports/{id}/intervention-link`
create/rotate public intervention token for current assignment
- `DELETE /reports/{id}/intervention-link`
revoke active intervention token
## Moderation
- `POST /reports/{id}/flags`
operator flags owned report
- `POST /reports/{id}/messages/{message_id}/flags`
operator flags owned report message
- `GET /admin/moderation`
admin moderation queue
- `PATCH /admin/moderation/{id}`
admin resolves/dismisses; may also reject report
## Admin Reference Data
- `POST /admin/categories`
- `PATCH /admin/categories/{id}`
- `DELETE /admin/categories/{id}`
- `POST /admin/areas`
- `PATCH /admin/areas/{id}`
- `DELETE /admin/areas/{id}`
deletes area and clears linked `reports.area_id` and `teams.area_id`
area polygon must stay inside allowed Bari municipal boundary
- `POST /admin/teams`
- `PATCH /admin/teams/{id}`
- `DELETE /admin/teams/{id}`
- `GET /admin/users`
- `PATCH /admin/users/{id}/roles`
- `PATCH /admin/users/{id}/ban`
- `GET /admin/stats/overview`
## Workflow Summary
### Citizen
1. register/login
2. create `OPEN` report
3. watch status/history/media
4. chat with operator
5. receive notifications
6. leave feedback on `RESOLVED`
### Operator
1. inspect open queue
2. `take` or assign team directly
3. own report through `assigned_operator_id`
4. update status / chat / moderate
5. generate public team link
### Admin
1. manage reference data and users
2. resolve moderation
3. read global stats
## Current Status Machine
- `OPEN -> UNDER_REVIEW | REJECTED`
- `UNDER_REVIEW -> ASSIGNED | REJECTED`
- `ASSIGNED -> IN_PROGRESS | REJECTED`
- `IN_PROGRESS -> RESOLVED | REJECTED`
- `RESOLVED -> CLOSED`
- `REJECTED -> CLOSED`
## Important Current Truths
- External teams are not platform users.
- Platform owner is always `reports.assigned_operator_id`.
- `assignments` is historical external team linkage only.
- Feedback does not auto-close report.
- Public intervention summary exposes citizen phone by design, but not email or full surname.
- Report coordinates are constrained to Bari municipal boundary.
- Admin-created area polygons must stay inside Bari municipal boundary.
+105
View File
@@ -0,0 +1,105 @@
# Visione Backend Architecture
## Purpose
Visione backend is C++20 + Drogon API for municipal urban issue reporting.
Citizens create reports. Operators own report workflow on platform. External
intervention teams do not log into platform; operators can assign them and
generate public read-only intervention links.
## Source Of Truth
- Route truth: `controllers/*.h`
- Business truth: `services/*.cc`
- Schema truth: `migrations/*.sql`
- Contract truth: `docs/openapi/visione.openapi.yaml` merged with route scan
- High-signal human summary: `docs/API_SPEC.md` and `docs/LLM_CONTEXT.md`
## Code Layout
- `controllers/`: HTTP input/output only, auth gate, status mapping.
- `services/`: business rules, workflow, permission checks, orchestration.
- `repositories/`: SQL serialization helpers and migration runner.
- `common/`: auth, crypto, validation, HTTP helpers, email, CORS.
- `migrations/`: ordered SQL schema evolution.
- `docs/openapi/`: manual OpenAPI layer.
- `scripts/generate_openapi.py`: extracts Drogon routes and merges manual spec.
## Runtime Model
- Auth uses bearer tokens from `api_tokens`.
- OTP login uses `otp_challenges` and Mailpit in local dev.
- CIE login uses external Django RP service:
- Visione creates local `state`
- Django RP completes CIE/OIDC and sends signed identity payload to Visione
- Visione creates or links local user, issues `api_tokens`, frontend polls session status
- DB access uses raw parameterized SQL through `drogon::orm::DbClient`.
- No Drogon generated ORM models. No `Mapper<T>`.
## Role Model
- `citizen`: own reports only, own messages, feedback, notifications.
- `operator`: can read all reports, manage workflow, assign teams, create flags.
- `admin`: full admin + report management + moderation approval.
Effective permission helpers:
- `auth::canManageReports(user)`: admin or `reports.manage`
- `auth::canOperateReports(user)`: non-admin report manager
## Workflow Ownership Rules
- `reports.assigned_operator_id` is single platform owner of report.
- `assignments` is external team assignment history only.
- Team assignment can claim `OPEN` report directly:
operator becomes `assigned_operator_id`, report moves to `ASSIGNED`.
- `take` exists for `OPEN -> UNDER_REVIEW` without team assignment.
- External team link is public, revocable, tied to current assignment, hashed in
`intervention_access_links`.
- Spatial scope is constrained to Bari municipal boundary from
`allowed_boundaries.code = 'BARI'`.
## Status Machine
- Allowed statuses: `OPEN`, `UNDER_REVIEW`, `ASSIGNED`, `IN_PROGRESS`,
`RESOLVED`, `REJECTED`, `CLOSED`
- Allowed transitions:
- `OPEN -> UNDER_REVIEW | REJECTED`
- `UNDER_REVIEW -> ASSIGNED | REJECTED`
- `ASSIGNED -> IN_PROGRESS | REJECTED`
- `IN_PROGRESS -> RESOLVED | REJECTED`
- `RESOLVED -> CLOSED`
- `REJECTED -> CLOSED`
## Main Persistence Objects
- `users`, `roles`, `permissions`, `user_roles`, `role_permissions`
- `api_tokens`, `otp_challenges`, `cie_auth_sessions`, `user_external_identities`
- `report_categories`, `areas`, `team_categories`, `teams`
- `reports`, `report_status_history`, `assignments`, `report_photos`
- `messages`, `feedback`, `reward_events`, `notifications`
- `moderation_flags`
- `report_category_team_categories`
- `intervention_access_links`
- `allowed_boundaries`
## HTTP/Error Conventions
- `401`: missing/invalid auth
- `403`: authenticated but forbidden
- `404`: missing resource or invalid public intervention token
- `409`: workflow/ownership conflict
- `422`: semantic payload/reference error
Controllers currently map service error strings heuristically. When adding new
service errors, update controller mappers too.
## OpenAPI Generation
`scripts/generate_openapi.py` scans `ADD_METHOD_TO(...)` in controller headers.
Generated route skeletons merge with `docs/openapi/visione.openapi.yaml`.
Build fails if manual spec contains operation without matching route, unless
`x-manual-only: true`.
Output path:
- `build/openapi/visione.openapi.json`
## Current Gaps / Sharp Edges
- No dedicated repository layer split yet; SQL still lives inside services.
- `AuthService::editUser()` updates fields with dynamic SQL column name from
hardcoded allowlist, not user input.
- Public intervention summary currently excludes email and full surname, but
still exposes citizen phone by design.
- Allowed-area containment for admin areas is vertex-based, simple not strict GIS-hard.
- No frontend doc set here beyond API/OpenAPI docs.
- CIE bridge trusts HMAC-signed callback from Django RP service; keep shared
secret rotated and never expose it to frontend.
+182
View File
@@ -0,0 +1,182 @@
# Visione CIE Authentication Integration
## Goal
Keep Visione backend simple.
Visione does not implement OIDC Federation or CIE protocol details directly.
`spid-cie-oidc-django` handles CIE login. Visione only manages:
- local login `state`
- user provisioning
- external identity linking
- local bearer token issuance
## Components
- Visione backend: `POST /api/v1/auth/cie/start`, `POST /api/v1/auth/cie/exchange`,
`GET /api/v1/auth/cie/session/{state}`
- Django RP service: `examples/relying_party`
- Frontend: browser redirect + polling
## End-to-End Flow
1. Frontend calls `POST /api/v1/auth/cie/start` with `frontend_redirect_uri`.
2. Visione stores row in `cie_auth_sessions` and returns `authorization_url`.
3. Frontend redirects browser to returned Django URL.
4. Django RP performs CIE login with provider.
5. On success Django extracts user attributes and sends signed JSON payload to
Visione `POST /api/v1/auth/cie/exchange`.
6. Visione verifies HMAC signature, validates `state`, finds or creates user,
stores `user_external_identities`, issues `api_tokens`, marks session authenticated.
7. Visione responds to Django with HTTP redirect to
`frontend_redirect_uri?state=<state>`.
8. Frontend reads `state` from query string and polls
`GET /api/v1/auth/cie/session/{state}` until authenticated.
9. Frontend stores returned Visione bearer token and calls `GET /api/v1/me`.
## Why This Architecture
- No OIDC/Federation crypto in C++ backend.
- No CIE protocol tables spread across Visione domain code.
- Django demo repo stays reusable and isolated.
- Visione keeps final auth authority for application tokens and user model.
## Visione Backend Config
`config.json`:
```json
"cie_auth": {
"service_base_url": "http://127.0.0.1:8001",
"start_path": "/visione/start",
"shared_secret": "change-me-for-local-dev",
"state_ttl_minutes": 10,
"default_frontend_redirect_uri": "http://localhost:3000/auth/cie/callback"
}
```
Meaning:
- `service_base_url`: Django RP base URL
- `start_path`: Django entrypoint used by frontend redirect
- `shared_secret`: HMAC secret shared only between Visione and Django
- `state_ttl_minutes`: local login state lifetime
- `default_frontend_redirect_uri`: fallback if frontend omits value
## Visione DB Additions
- `cie_auth_sessions`
stores local state, frontend redirect, final user id, final bearer token
- `user_external_identities`
stores `provider`, `issuer`, `subject`, `fiscal_code`, raw claims
User matching strategy:
1. exact external identity on `provider + issuer + subject`
2. fallback reunification on `fiscal_code`
3. otherwise create new `citizen`
## Signed Exchange Contract
Django sends `POST /api/v1/auth/cie/exchange` with JSON:
```json
{
"state": "visione-login-state",
"issuer": "http://127.0.0.1:8002/oidc/op",
"subject": "provider-subject",
"fiscal_code": "RSSMRA80A01H501U",
"first_name": "Mario",
"last_name": "Rossi",
"email": "[email protected]"
}
```
Headers:
- `Content-Type: application/json`
- `X-Visione-Signature: <UPPERCASE_HEX_HMAC_SHA256>`
Signature algorithm:
- payload bytes exactly as sent
- key = `cie_auth.shared_secret`
- HMAC-SHA256
- uppercase hex output
## Django Example Customization
Modified files in `spid-cie-oidc-django/examples/relying_party`:
- `relying_party/settingslocal.py.example`
- `relying_party/urls.py`
- `relying_party/visione_views.py`
Added endpoints:
- `GET /visione/start`
stores Visione `state` in Django session, then redirects to `/oidc/rp/authorization`
- `GET /visione/complete`
reads `oidc_rp_user_attrs`, signs payload, calls Visione exchange endpoint,
then redirects browser to frontend callback URL returned by Visione
Important:
- default OIDC callback remains managed by `spid_cie_oidc.relying_party`
- `LOGIN_REDIRECT_URL` now points to `/visione/complete`
- no need to modify RP metadata dump callback URI for this integration
## Frontend Responsibilities
1. Call:
```http
POST /api/v1/auth/cie/start
Content-Type: application/json
```
```json
{
"frontend_redirect_uri": "http://localhost:3000/auth/cie/callback"
}
```
2. Redirect browser to `authorization_url`.
3. On frontend callback page, read `state` from query string.
4. Poll:
```http
GET /api/v1/auth/cie/session/{state}
```
5. Handle states:
- `pending`: keep polling
- `authenticated`: save `auth.token`
- `failed`: show error and restart flow
6. After token, call `GET /api/v1/me`.
Frontend does not need:
- CIE secrets
- Django shared secret
- OIDC token parsing
## Local Dev Setup
1. Start Visione dependencies:
```bash
docker compose up -d
```
2. Run Visione:
```bash
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./build/visione-backend
```
3. In separate repo checkout for `spid-cie-oidc-django`, configure:
- federation authority example
- provider example
- relying party example with `VISIONE_BACKEND_EXCHANGE_URL`
- same `VISIONE_SHARED_SECRET` as Visione `cie_auth.shared_secret`
4. Start Django RP on `http://127.0.0.1:8001`.
## Security Notes
- Shared secret must never reach frontend.
- `cie_auth.shared_secret` must change outside local dev.
- Keep `state_ttl_minutes` short.
- Visione bearer token stays out of query string; only `state` goes to frontend.
- Django service is trusted bridge. Protect it as internal auth component.
## Future Improvements
- Mark failed sessions explicitly when Django login fails
- Rotate HMAC secret through environment variables
- Add replay protection with timestamp/nonce in signed payload
- Add integration tests between Django RP and Visione
+115
View File
@@ -0,0 +1,115 @@
# Visione Database Schema
Schema truth is `migrations/*.sql`. This file is compact human map of current DB.
## Migration Set
- `001_initial_schema.sql`
base auth, users, reports, areas, teams, messages, feedback, rewards
- `004_report_workflow.sql`
`reports.assigned_operator_id`, workflow indexes, richer media columns
- `006_completion_features.sql`
notifications, moderation flags, reward uniqueness
- `007_reference_data_and_intervention_links.sql`
category-to-team-category mapping, public intervention links
- `011_cie_auth_bridge.sql`
lightweight CIE bridge sessions and external identity links
## Core Auth Tables
- `users`
identity, profile, points, `banned`
- `roles`
- `permissions`
- `role_permissions`
- `user_roles`
- `otp_challenges`
OTP flow, hashed codes, expiry, consumption
- `api_tokens`
bearer token hashes, expiry, revocation
- `cie_auth_sessions`
frontend-visible CIE login state, final issued token, expiry
- `user_external_identities`
links local users to external providers using `provider + issuer + subject`
## Reference Data
- `report_categories`
citizen-facing report categories, `active`
- `areas`
named municipal areas, GeoJSON polygon in `boundary_geojson`
- `team_categories`
category of external intervention team
- `teams`
external teams, optional `area_id`, `team_category_id`, `active`
- `report_category_team_categories`
explicit compatibility mapping:
report category -> team category
- `allowed_boundaries`
active allowed municipal containment geometry, currently seeded with `BARI`
## Report Domain
- `reports`
owner citizen, category, inferred area, coordinates, address, description,
status, `assigned_operator_id`
- `report_status_history`
immutable workflow log
- `assignments`
historical report -> external team assignments, active row has `ended_at IS NULL`
- `report_photos`
historical name; stores both images and videos
current columns include `original_filename`, `content_type`, `media_kind`,
`size_bytes`
- `messages`
citizen/operator conversation per report
- `feedback`
one row per report, unique on `report_id`
## Engagement / Moderation
- `reward_events`
points ledger, unique `(report_id, reason)`
- `notifications`
local in-app notifications
- `moderation_flags`
operator-created flags, admin-resolved moderation queue
- `intervention_access_links`
public revocable hashed links for external teams, tied to `report_id` and
`assignment_id`
## Ownership Model
- Platform owner of report: `reports.assigned_operator_id`
- External team linkage: `assignments`
- Public field-team access: `intervention_access_links`
## Status Model
`reports.status` enum:
- `OPEN`
- `UNDER_REVIEW`
- `ASSIGNED`
- `IN_PROGRESS`
- `RESOLVED`
- `REJECTED`
- `CLOSED`
## Important Constraints / Semantics
- `users.email` unique
- `users.fiscal_code` unique
- `teams.name` unique
- `areas.name` unique
- `report_categories.name` unique
- `feedback.report_id` unique
- `reward_events(report_id, reason)` unique
- `api_tokens.token_hash` unique
- `intervention_access_links.token_hash` unique
- category/team delete behavior is soft delete via `active = FALSE`
- area delete behavior is hard delete; linked `reports.area_id` and `teams.area_id`
become `NULL`
## Current Query Model
- No generated ORM models
- No `Mapper<T>`
- Raw parameterized SQL through `drogon::orm::DbClient`
- JSON serialization helpers in `repositories/Sql.*`
## Current Gaps
- No dedicated table for external team personnel
- No route-optimization/routing tables
- No audit table for intervention link access beyond `last_accessed_at`
- Containment check for admin area polygons is simple vertex-inside check
+137
View File
@@ -0,0 +1,137 @@
# Visione Backend LLM Context
## What This Repo Is
Visione is C++20 + Drogon backend for urban issue reporting.
Core actors:
- `citizen`: creates and follows reports
- `operator`: manages report lifecycle on platform
- `admin`: manages users/reference data and approves moderation
External intervention teams are off-platform. They receive summary through
public revocable link generated by operator/admin.
## Environment
- Local backend runs outside Docker.
- Docker is only for local dependencies like MySQL and Mailpit.
- Primary commands:
- configure: `cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug`
- build: `cmake --build build`
- test: `ctest --test-dir build --output-on-failure`
- openapi: `cmake --build build --target openapi`
## High-Signal Conventions
- Code, DB identifiers, API fields, comments: English.
- User-facing labels may be Italian.
- Keep controller thin, service owns business logic.
- Use parameterized SQL only.
- Prefer current code over old assumptions. Route truth is in controller headers.
## Current Auth Model
- Registration creates `citizen`.
- Login path:
- OTP email via Mailpit
- CIE via external `spid-cie-oidc-django` relying party service
- Auth token: bearer token from `api_tokens`
- Banned users cannot authenticate.
### CIE Bridge Flow
1. Frontend calls `POST /api/v1/auth/cie/start`.
2. Visione creates `cie_auth_sessions.state` and returns Django start URL.
3. Browser completes CIE login on Django RP service.
4. Django sends signed payload to `POST /api/v1/auth/cie/exchange`.
5. Visione finds or creates user, stores `user_external_identities`, issues token.
6. Frontend polls `GET /api/v1/auth/cie/session/{state}` until `authenticated`.
## Current Report Workflow
### Citizen flow
1. Register/login.
2. `POST /reports` with category, coordinates, address, description, optional media array.
3. Backend first rejects coordinates outside Bari municipal boundary.
4. Backend infers `area_id` by GeoJSON polygon match when possible from existing areas.
5. Report starts `OPEN`, status history row inserted.
6. Citizen can:
- list own reports
- read detail/history/media
- chat on own report
- upload more media up to 5 total
- read notifications
- read rewards
- leave feedback only when report is `RESOLVED`
### Operator flow
1. Read queues:
- `/operator/reports/open`
- `/operator/reports/assigned`
- `/operator/reports/others`
2. Optional `POST /reports/{id}/take`:
`OPEN -> UNDER_REVIEW`, owner becomes operator.
3. `POST /reports/{id}/assignments`:
- if report `OPEN`, operator can assign directly without `take`
- owner becomes operator if report had no owner
- status becomes `ASSIGNED`
- current active assignment closes, new one inserted
4. Continue status flow:
- `ASSIGNED -> IN_PROGRESS -> RESOLVED -> CLOSED`
- rejection path to `REJECTED`, then `CLOSED`
5. Operator can chat with citizen on owned report.
6. Operator can create moderation flags for report/message on owned report.
7. Operator can request team suggestions and generate/revoke public intervention link.
### Admin flow
- Create/update categories, areas, teams
- Area GeoJSON must stay inside Bari municipal boundary
- List users, edit roles, ban/unban
- Read stats
- Review moderation queue and resolve/dismiss flags
- Optionally reject report during moderation resolution
## Team Model
- `teams` are external intervention squads, not platform users.
- `assignments` is history of which external team was assigned.
- `report_category_team_categories` maps report categories to compatible team categories.
- `GET /reports/{id}/team-suggestions` returns recommended active teams by:
- mapped team category
- same area first
- area-less team as fallback
- `POST /reports/{id}/intervention-link` creates revocable public link.
- `GET /interventions/{token}` returns public summary for field team:
report data, location, media, assigned team, masked citizen identity, phone.
## Spatial Boundary Rule
- `allowed_boundaries` stores active municipal allowed area.
- Current seed uses `BARI`.
- Report coordinates outside boundary are rejected.
- Admin area polygons outside boundary are rejected.
- Open data output is filtered again by same boundary.
## Current Notifications / Rewards / Moderation
- Notifications: local in-app only.
- Reward points: awarded once per `(report_id, reason)`.
- Moderation:
- operator creates flag
- admin resolves/dismisses
- admin may reject report during moderation decision
## Important Behavioral Truths
- Owner on platform is always `reports.assigned_operator_id`, not team.
- Feedback does not auto-close report.
- Public areas list is anonymous.
- Report categories require auth.
- Team list requires operator/admin auth.
- Moderation approval is admin-only.
- Public intervention token invalid/revoked/scoped-out returns `404`.
## Files To Read First
- `docs/API_SPEC.md`
- `docs/ARCHITECTURE.md`
- `docs/CIE_AUTH_INTEGRATION.md`
- `docs/openapi/visione.openapi.yaml`
- `controllers/*.h`
- `services/*.cc`
- `migrations/*.sql`
## External References
- Local Drogon source: `/home/gabdevele/Projects/snitchitaly/backend/drogon`
- Drogon docs: `https://drogonframework.github.io/drogon-docs/`
- Durable agent instructions: `AGENTS.md`
+129
View File
@@ -0,0 +1,129 @@
# Visione Project Spec
This file describes current implemented product shape, not aspirational backlog.
## Goal
Visione lets citizens report urban issues and lets municipal staff manage report
workflow, communicate with citizens, assign real external intervention teams,
moderate abuse, and inspect operational stats.
## Roles
- `citizen`
register, login, create reports, read own reports, chat, upload media,
read notifications, read rewards, leave feedback on resolved reports
- `operator`
read all reports, take open reports, own workflow, assign external teams,
change statuses, chat with citizens, create moderation flags, generate public
intervention links
- `admin`
all operator powers plus user/role management, reference data management,
moderation approval, bans, stats
## Product Model
- Citizens use platform directly.
- Operators use platform directly.
- External intervention teams do not authenticate to platform.
- Teams receive public read-only intervention summary through revocable link.
## Implemented Features
### Auth
- citizen registration
- OTP login through Mailpit
- bearer token auth
- profile read + limited self-edit (`first_name`, `last_name`, `phone`)
### Reports
- create report with category, coordinates, address, description
- optional inline media on creation
- later media upload up to total 5 files
- coordinates must stay inside Bari municipal boundary
- area inferred by GeoJSON polygon match
- citizen report list/detail/history/media
- operator queues:
- open unassigned
- assigned to current operator
- assigned to other operators
### Workflow
- statuses:
`OPEN`, `UNDER_REVIEW`, `ASSIGNED`, `IN_PROGRESS`, `RESOLVED`, `REJECTED`, `CLOSED`
- valid transitions enforced in backend
- `take` claims open report into `UNDER_REVIEW`
- team assignment can directly claim open report and move to `ASSIGNED`
- owner on platform remains operator, not team
### Communication
- citizen/operator report chat
- local in-app notifications for status, assignment, messages, rewards
### External Team Handling
- external teams are reference data, not platform accounts
- active teams filtered for operator list
- suggestion endpoint recommends compatible teams by category mapping and area
- operator/admin can create or revoke public intervention summary link
- public summary exposes operational info needed on field:
location, description, media, assigned team, masked citizen identity, phone
### Moderation
- operator can flag report or message on owned report
- admin resolves or dismisses flag
- admin can reject report during moderation decision
### Rewards / Feedback
- reward events ledger
- points awarded on resolved/closed report, deduplicated by `(report_id, reason)`
- citizen feedback allowed only on `RESOLVED`
- feedback does not auto-close report
### Admin
- create/update categories
- soft-delete categories
- create/update areas
area polygons must stay inside Bari municipal boundary
- delete areas
- create/update teams
- soft-delete teams
- list users, update roles, ban/unban
- global stats including area/category/team resolution aggregates
### Public Data
- area list
- anonymized resolved/closed open-data export
- public intervention token endpoint
- open-data output filtered to Bari municipal boundary
## Current Workflow Summary
### Citizen
1. register/login
2. create report
3. report starts `OPEN`
4. follow history/status
5. chat with operator if needed
6. receive notifications
7. leave feedback when resolved
### Operator
1. inspect open queue
2. `take` or assign external team directly
3. become report owner on platform
4. update status / chat / flag abuse
5. generate public intervention link for team
### Admin
1. manage users and reference data
2. approve moderation decisions
3. inspect stats
## Out Of Scope Today
- real CIE/SPID integration
- production email/SMS/Telegram/WhatsApp notifications
- routing optimization for teams
- team platform accounts
- citizen-facing frontend in this repo
- hard-delete lifecycle for reference data
## Product Truth Rules
- Code truth beats stale docs.
- Route truth: controller headers.
- Business truth: services.
- DB truth: migrations.
+167
View File
@@ -0,0 +1,167 @@
Traccia principale del progetto:
Caso di studio
Segnalazioni urbane
Un’amministrazione comunale desidera sviluppare un sistema digitale per permettere ai cittadini di segnalare problemi presenti sul territorio, come buche stradali, illuminazione guasta, rifiuti abbandonati o altri disservizi urbani, con l’obiettivo di migliorare la gestione degli interventi e la qualità della vita in città.
Il sistema deve prevedere due principali tipologie di utenti: da un lato i cittadini, che inviano segnalazioni, dall’altro gli operatori comunali, responsabili della gestione e risoluzione degli interventi.
I cittadini devono potersi registrare e accedere tramite credenziali personali. Una volta autenticati, devono poter inserire una nuova segnalazione specificando il tipo di problema, la posizione e una breve descrizione. Il sistema deve memorizzare le segnalazioni e permettere ai cittadini di visualizzarne lo stato nel tempo, ad esempio in attesa, in lavorazione o risolta.
Gli utenti devono inoltre poter consultare le segnalazioni già presenti, eventualmente filtrandole per categoria o area, e fornire un feedback sull’efficacia degli interventi effettuati.
Dal lato degli operatori comunali, il sistema deve consentire di visualizzare tutte le segnalazioni ricevute, assegnarle a specifiche squadre di intervento e aggiornarne lo stato man mano che vengono gestite. Deve essere possibile monitorare i tempi di risposta e individuare le aree della città con maggior concentrazione di problemi.
Il sistema deve mantenere uno storico delle segnalazioni e degli interventi effettuati, permettendo all’amministrazione di analizzare i dati raccolti per pianificare interventi futuri e migliorare l’efficienza dei servizi.
Dopodichè ho elaborato una serie di feature che il sistema deve implementare anche rielaborazione e idee. ovvero:
Feature Segnalazioni urbane
– Lo sviluppo del della webapp avverrà su una istanza self hosted di Gitea come gesto di ribellione nei confronti di github che userà i dati sulle repo per trainare i modelli di IA (lo faceva già prima ma ora lo dice)
– Il progetto sarà diviso tra frontend e backend. Frontend in probabilmente Vue.js e backend in C++, nello specifico Dragon è stato scelto come framework. Abbiamo scelto di sviluppare una webapp perchè il punto importante di un progetto del genere è la versatilità, la compatibilità su tutti i sistemi e la velocità.
– Deve esistere un account amministratore generato dal sistema. Questo profilo lo dovrà teoricamente avere solo chi il tecnico che gestisce la piattaforma di quel comune, dato che da questo profilo si può fare tutto. E’ diverso da un account operatore perchè l’operatore ha meno permessi rispetto ad un admin, l’admin può decidere chi sono gli operatori. (NOTA: eventualmente ci possono essere i permessi customizzabili, quindi l’admin può creare anche altri utenti che hanno permessi in più)
– Deve essere possibile registrarsi alla piattaforma con tutti i quanti i dati anagrafici possibili e immaginabili, sarebbe figo avere un sistema che dato il codice fiscale ti auto completa i dati deducibili da quest’ultimo. Integrazione frontend con google maps per l’indirizzo. Deve essere possibile anche collegare CIE. Nella registrazione si richiede verifica a due fattori
– Il login deve essere effettuato con email o CIE. Nel caso di email la modalità di default usa OTP, in quanto dimostrato essere più sicuro.
– Una volta registrati, se siamo un operatore (comunale, poliziotto, pompiere o chiunque) sarà un admin o un’altro operatore con abbastanza permessi ad assegnarci a quel ruolo e sbloccare i permessi per accedere ad altre parti della piattaforma.
– Admin/operatori vari devono poter creare le varie zone in base al perimetro, se questo è fattibile tramite una mappa sarebbe l’ideale, assegnare i vari nomi ecc.. Questo serve sia per le segnalazioni e per l’assegnazione dell’agente/squadra che deve intervenire sia per fini statistici
– Al sito non si registrano solo gli agenti che intervengono o gli operatori ma anche i cittadini, che possono effettuare delle segnalazioni. Le segnalazioni devono avere una foto, la posizione con coordinate geografiche e via con numero. Il sistema automaticamente capisce a che zona del comune fa parte
– Quando viene creata una segnalazione c’è un indicatore dello stato, tipo: APERTA, IN REVISIONE, AGENTI IN ARRIVO, RISOLTA (sono esempi, vanno migliorati). Di default viene assegnato APERTA.
– Nella segnalazione c’è una chat che viene fatta tra l’operatore e il cittadino per fare eventuali domande. L’operatore quindi può aggiornare lo stato, scrivere messaggi, segnalare il cittadino perchè magari ha inviato un pene come segnalazione.
– L’agente riceverà un summary della segnalazione + numero di telefono di chi ha segnalato, dato che intervengono sul posto non serve che loro chattano, chiamano direttamente.
– La webapp deve avere una pagina con una serie di statistiche
– Si può implementare anche una specie di gamification (indiretta, non diciamolo in maniera ovvia nel sito) del sistema per renderlo più interattivo, tipo ogni segnalazione valida vale dei punti che se accumulati portano a buoni / sconti.
– L’utente e gli operatori devono poter aggiungere anche dei filtri quando si cercano le varie segnalazioni. Inoltre il cittadino deve anche poter lasciare un feedback sull’intervento che hanno fatto, questo sarà collegato quindi anche all’operatore e agli agenti della squadra che sono intervenuti.
– L’operatore deve poter vedere le varie segnalazioni che sta gestendo e anche quelle di altri operatori ma senza poterci interagire / modificarle.
– Tutti vedono e possono prendere in carico le segnalazioni aperte.
In aggiunta come feature opzionali e secondarie sarebbe carino implementare:
- Notifiche Multicanale: Obbligare il cittadino a rientrare nell'app per controllare lo stato della segnalazione genera frustrazione. Potresti implementare notifiche via Email o l'integrazione con un Bot Telegram/WhatsApp che avvisa automaticamente l'utente quando lo stato passa da "In Lavorazione" a "Risolta".
- Routing Intelligente: Sfruttando le API delle mappe, il sistema potrebbe calcolare per la squadra operativa il percorso ottimale per risolvere 4 o 5 segnalazioni assegnate nella stessa giornata, minimizzando i tempi di spostamento
- Portale Open Data: Le amministrazioni amano promuovere la trasparenza. Potresti creare una pagina pubblica dove chiunque (cittadini, giornalisti, ricercatori) può scaricare un dataset anonimizzato con l'elenco dei disservizi risolti nell'ultimo anno, esportabile in formati standard come JSON o CSV.
Un mio collega ha definito questo schema per il database, possiamo e dobbiamo sicuramente modificarlo ma può essere un buon punto di partenza:
erDiagram
UTENTE ||--o{ SEGNALAZIONE : "manda"
%%citatdino
UTENTE ||--o{ STORICO_STATO : "aggiorna"
%%operatore
UTENTE ||--o{ ASSEGNAZIONE : "assegna "
%%operatore
UTENTE ||--o{ FEEDBACK : "lascia "
%%citatdino
UTENTE ||--o{ MESSAGGIO : "scrive"
CATEGORIA_SEGNALAZIONE ||--o{ SEGNALAZIONE : "classifica"
AREA ||--o{ SEGNALAZIONE : "localizza"
AREA ||--o{ SQUADRA : "ha"
%%che relaziobne diu merda
CATEGORIA_SQUADRA ||--o{ SQUADRA : "ha"
SEGNALAZIONE ||--o{ STORICO_STATO : "traccia"
SEGNALAZIONE ||--o{ ASSEGNAZIONE : "riceve"
SEGNALAZIONE ||--o| FEEDBACK : "valutata"
SEGNALAZIONE ||--o{ FOTO_SEGNALAZIONE : "documentata"
SEGNALAZIONE ||--o{ MESSAGGIO : "discussa"
SQUADRA ||--o{ ASSEGNAZIONE : "incaricata"
UTENTE {
int id PK
string nome
string cognome
string email UK
string codice_fiscale UK
date data_nascita
string luogo_nascita
enum ruolo
string telefono
datetime data_registrazione
int punti
boolean bannato
}
CATEGORIA_SEGNALAZIONE {
int id PK
string nome
text descrizione
}
AREA {
int id PK
string nome_quartiere
text descrizione
geometry coordinate_poligono
%%bho
}
CATEGORIA_SQUADRA {
int id PK
string nome
text descrizione
}
SEGNALAZIONE {
int id PK
int utente_id FK
int categoria_id FK
int area_id FK
decimal latitudine
decimal longitudine
string indirizzo
text descrizione
enum stato
datetime data_creazione
datetime data_ultima_modifica
}
STORICO_STATO {
int id PK
int segnalazione_id FK
datetime data
int operatore_id FK
text note
}
SQUADRA {
int id PK
int categoria_squadra_id FK
int area_id FK
string nome
text descrizione
boolean attiva
}
ASSEGNAZIONE {
int id PK
int segnalazione_id FK
int squadra_id FK
int operatore_assegnante_id FK
datetime data_assegnazione
datetime data_cessazione
}
FEEDBACK {
int id PK
int segnalazione_id FK, UK
int utente_id FK
int voto
%%??
text commento
datetime data
}
FOTO_SEGNALAZIONE {
int id PK
int segnalazione_id FK
string url
datetime data_upload
}
MESSAGGIO {
int id PK
int segnalazione_id FK
int utente_id FK
text testo
datetime data_invio
}
+2 -2
View File
@@ -2582,7 +2582,7 @@
}, },
"/api/v1/auth/otp/request": { "/api/v1/auth/otp/request": {
"post": { "post": {
"description": "Sends a one-time code by email through configured SMTP provider.", "description": "Sends a one-time code by email through Mailpit locally.",
"operationId": "AuthController.requestOtp", "operationId": "AuthController.requestOtp",
"parameters": [ ], "parameters": [ ],
"requestBody": { "requestBody": {
@@ -3312,7 +3312,7 @@
}, },
"/api/v1/reports": { "/api/v1/reports": {
"get": { "get": {
"description": "Lists reports visible to the authenticated user. Citizens see own reports only; operators and admins can read all reports. Admin can use this endpoint for global read-only overview screens.", "description": "Lists reports visible to the authenticated user.",
"operationId": "ReportController.list", "operationId": "ReportController.list",
"parameters": [ "parameters": [
{ {
+3 -54
View File
@@ -5,24 +5,8 @@
#include "repositories/MigrationRunner.h" #include "repositories/MigrationRunner.h"
#include <cstdio> #include <cstdio>
#include <cstdlib>
#include <fstream> #include <fstream>
#include <string_view> #include <cstdlib>
namespace
{
bool parseEnvBool(const char *value)
{
if (value == nullptr)
{
return false;
}
const std::string_view normalized(value);
return normalized == "1" || normalized == "true" || normalized == "TRUE" ||
normalized == "yes" || normalized == "on";
}
} // namespace
int main() int main()
{ {
@@ -35,7 +19,6 @@ int main()
{ {
configFile >> config; configFile >> config;
// DB config from env
if (const char* envHost = std::getenv("DB_HOST")) if (const char* envHost = std::getenv("DB_HOST"))
config["db_clients"][0]["host"] = envHost; config["db_clients"][0]["host"] = envHost;
if (const char* envPort = std::getenv("DB_PORT")) if (const char* envPort = std::getenv("DB_PORT"))
@@ -49,46 +32,12 @@ int main()
if (const char* envTimeout = std::getenv("DB_TIMEOUT")) if (const char* envTimeout = std::getenv("DB_TIMEOUT"))
config["db_clients"][0]["timeout"] = std::stod(envTimeout); config["db_clients"][0]["timeout"] = std::stod(envTimeout);
// Mail config from env
if (const char* envMailProvider = std::getenv("MAIL_PROVIDER"))
config["custom_config"]["mail"]["provider"] = envMailProvider;
if (const char* envSmtpHost = std::getenv("SMTP_HOST")) if (const char* envSmtpHost = std::getenv("SMTP_HOST"))
config["custom_config"]["mail"]["host"] = envSmtpHost; config["custom_config"]["mail"]["smtp_host"] = envSmtpHost;
if (const char* envSmtpPort = std::getenv("SMTP_PORT")) if (const char* envSmtpPort = std::getenv("SMTP_PORT"))
config["custom_config"]["mail"]["port"] = std::stoi(envSmtpPort); config["custom_config"]["mail"]["smtp_port"] = std::stoi(envSmtpPort);
if (const char* envSmtpUsername = std::getenv("SMTP_USERNAME"))
config["custom_config"]["mail"]["username"] = envSmtpUsername;
if (const char* envSmtpPassword = std::getenv("SMTP_PASSWORD"))
config["custom_config"]["mail"]["password"] = envSmtpPassword;
else if (const char* envResendApiKey = std::getenv("RESEND_API_KEY"))
config["custom_config"]["mail"]["password"] = envResendApiKey;
if (const char* envSmtpUseTls = std::getenv("SMTP_USE_TLS"))
config["custom_config"]["mail"]["use_tls"] = parseEnvBool(envSmtpUseTls);
if (const char* envSmtpUseStartTls = std::getenv("SMTP_USE_STARTTLS"))
config["custom_config"]["mail"]["use_starttls"] = parseEnvBool(envSmtpUseStartTls);
if (const char* envSmtpVerifyPeer = std::getenv("SMTP_VERIFY_PEER"))
config["custom_config"]["mail"]["verify_peer"] = parseEnvBool(envSmtpVerifyPeer);
if (const char* envSmtpVerifyHost = std::getenv("SMTP_VERIFY_HOST"))
config["custom_config"]["mail"]["verify_host"] = parseEnvBool(envSmtpVerifyHost);
if (const char* envMailFrom = std::getenv("MAIL_FROM"))
config["custom_config"]["mail"]["from"] = envMailFrom;
// CORS config from env
if (const char* envCorsAllowedOrigin = std::getenv("CORS_ALLOWED_ORIGIN")) if (const char* envCorsAllowedOrigin = std::getenv("CORS_ALLOWED_ORIGIN"))
config["custom_config"]["cors"]["allowed_origin"] = envCorsAllowedOrigin; config["custom_config"]["cors"]["allowed_origin"] = envCorsAllowedOrigin;
// CIE auth config from env
if (const char* envCieServiceBaseUrl = std::getenv("CIE_AUTH_SERVICE_BASE_URL"))
config["custom_config"]["cie_auth"]["service_base_url"] = envCieServiceBaseUrl;
if (const char* envCieStartPath = std::getenv("CIE_AUTH_START_PATH"))
config["custom_config"]["cie_auth"]["start_path"] = envCieStartPath;
if (const char* envCieSharedSecret = std::getenv("CIE_AUTH_SHARED_SECRET"))
config["custom_config"]["cie_auth"]["shared_secret"] = envCieSharedSecret;
if (const char* envCieStateTtl = std::getenv("CIE_AUTH_STATE_TTL_MINUTES"))
config["custom_config"]["cie_auth"]["state_ttl_minutes"] = std::stoi(envCieStateTtl);
if (const char* envCieFrontendRedirect = std::getenv("CIE_AUTH_DEFAULT_FRONTEND_REDIRECT_URI"))
config["custom_config"]["cie_auth"]["default_frontend_redirect_uri"] = envCieFrontendRedirect;
drogon::app().loadConfigJson(config); drogon::app().loadConfigJson(config);
} }
else else
+5 -5
View File
@@ -224,13 +224,13 @@ INSERT IGNORE INTO users (
fiscal_code, fiscal_code,
phone phone
) VALUES ( ) VALUES (
'Vito', 'Gennario',
'Leccese', 'Vessio',
'admin@visionebari.it', 'admin@visione.gov.it',
'LCCVTI62M02A662Q', 'VSSGNR94M08A662P',
'380 6180216' '380 6180216'
); );
INSERT IGNORE INTO user_roles (user_id, role_id) INSERT IGNORE INTO user_roles (user_id, role_id)
SELECT u.id, r.id FROM users u JOIN roles r SELECT u.id, r.id FROM users u JOIN roles r
WHERE u.email = 'admin@visionebari.it' AND r.name = 'admin'; WHERE u.email = 'admin@visione.gov.it' AND r.name = 'admin';
+5 -5
View File
@@ -1,7 +1,7 @@
UPDATE users UPDATE users
SET email = 'admin@visionebari.it', SET email = 'admin@visione.gov.it',
first_name = 'Vito', first_name = 'Gennario',
last_name = 'Leccese', last_name = 'Vessio',
fiscal_code = 'LCCVTI62M02A662Q', fiscal_code = 'VSSGNR94M08A662P',
phone = '380 6180216' phone = '380 6180216'
WHERE email = 'admin@visionebari.it'; WHERE email IN ('admin@visione.local', '[email protected].it');
+5 -5
View File
@@ -1,7 +1,7 @@
UPDATE users UPDATE users
SET email = 'admin@visionebari.it', SET email = 'admin@visione.gov.it',
first_name = 'Vito', first_name = 'Gennario',
last_name = 'Leccese', last_name = 'Vessio',
fiscal_code = 'LCCVTI62M02A662Q', fiscal_code = 'VSSGNR94M08A662P',
phone = '380 6180216' phone = '380 6180216'
WHERE email = 'admin@visionebari.it'; WHERE email IN ('admin@visione.local', '[email protected].it');
@@ -0,0 +1,12 @@
ALTER TABLE teams DROP FOREIGN KEY teams_area_fk;
ALTER TABLE reports DROP FOREIGN KEY reports_area_fk;
ALTER TABLE areas DROP COLUMN active;
ALTER TABLE teams
ADD CONSTRAINT teams_area_fk
FOREIGN KEY (area_id) REFERENCES areas(id) ON DELETE SET NULL;
ALTER TABLE reports
ADD CONSTRAINT reports_area_fk
FOREIGN KEY (area_id) REFERENCES areas(id) ON DELETE SET NULL;
+3
View File
@@ -0,0 +1,3 @@
ALTER TABLE users
DROP COLUMN password_hash,
DROP COLUMN must_change_password;
+2
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Generate OpenAPI JSON from Drogon routes plus a manual OpenAPI YAML file."""
from __future__ import annotations from __future__ import annotations
import argparse import argparse
+2 -2
View File
@@ -328,12 +328,12 @@ Json::Value AuthService::requestOtp(const Json::Value &body)
email::EmailService mail; email::EmailService mail;
if (!mail.sendOtp(user[0]["email"].as<std::string>(), code)) if (!mail.sendOtp(user[0]["email"].as<std::string>(), code))
{ {
LOG_WARN << "Could not send OTP email through " << mail.deliveryName(); LOG_WARN << "Could not send OTP email through Mailpit";
return failure("otp_email_failed"); return failure("otp_email_failed");
} }
auto response = success(); auto response = success();
response["delivery"] = mail.deliveryName(); response["delivery"] = "mailpit";
response["expires_in_minutes"] = otpTtlMinutes(); response["expires_in_minutes"] = otpTtlMinutes();
return response; return response;
} }
-16
View File
@@ -7,7 +7,6 @@
#include "services/UserService.h" #include "services/UserService.h"
#include <drogon/drogon.h> #include <drogon/drogon.h>
#include <drogon/orm/Exception.h>
#include <map> #include <map>
#include <set> #include <set>
@@ -1058,16 +1057,6 @@ Json::Value ReportService::addFeedback(const auth::UserContext &user,
return failure("report_not_resolved"); return failure("report_not_resolved");
} }
const auto existingFeedback = db->execSqlSync(
"SELECT id FROM feedback WHERE report_id = ?",
reportId);
if (!existingFeedback.empty())
{
return failure("feedback_already_exists");
}
try
{
db->execSqlSync( db->execSqlSync(
"INSERT INTO feedback (report_id, user_id, rating, comment) " "INSERT INTO feedback (report_id, user_id, rating, comment) "
"VALUES (?, ?, ?, ?)", "VALUES (?, ?, ?, ?)",
@@ -1075,11 +1064,6 @@ Json::Value ReportService::addFeedback(const auth::UserContext &user,
user.id, user.id,
rating, rating,
body.get("comment", "").asString()); body.get("comment", "").asString());
}
catch (const drogon::orm::DrogonDbException &)
{
return failure("feedback_already_exists");
}
return success(); return success();
} }
-1
View File
@@ -11,7 +11,6 @@ target_include_directories(${PROJECT_NAME}_test
target_link_libraries(${PROJECT_NAME}_test target_link_libraries(${PROJECT_NAME}_test
PRIVATE PRIVATE
Drogon::Drogon Drogon::Drogon
CURL::libcurl
OpenSSL::Crypto OpenSSL::Crypto
) )
+2 -2
View File
@@ -55,7 +55,7 @@ DROGON_TEST(ValidationRejectsMalformedIdentity)
CHECK(visione::validation::isValidEmail("[email protected]")); CHECK(visione::validation::isValidEmail("[email protected]"));
CHECK(!visione::validation::isValidEmail("not-an-email")); CHECK(!visione::validation::isValidEmail("not-an-email"));
CHECK(visione::validation::isValidFiscalCode("RSSMRA80A01H501U")); CHECK(visione::validation::isValidFiscalCode("RSSMRA80A01H501U"));
CHECK(visione::validation::isValidFiscalCode("LCCVTI62M02A662Q")); CHECK(visione::validation::isValidFiscalCode("VSSGNR94M08A662P"));
CHECK(!visione::validation::isValidFiscalCode("short")); CHECK(!visione::validation::isValidFiscalCode("short"));
CHECK(!visione::validation::isValidFiscalCode("VSSGNR94M08A662A")); CHECK(!visione::validation::isValidFiscalCode("VSSGNR94M08A662A"));
} }
@@ -80,7 +80,7 @@ DROGON_TEST(SqlScalarSerializationKeepsTypes)
DROGON_TEST(CryptoHmacIsDeterministic) DROGON_TEST(CryptoHmacIsDeterministic)
{ {
const auto digest = visione::crypto::hmacSha256Hex("secret", "{\"a\":1}"); const auto digest = visione::crypto::hmacSha256Hex("secret", "{\"a\":1}");
CHECK(digest == "AA9E2E3575F5D7098B6CACCD790888C36D5FDB63342A73BADA2D6A51747A8494"); CHECK(digest == "AA9E2E3575F5D7098B6CACCDB4486EA647EFDC3E2CA6A5CEE762FCAFD14E3F14");
CHECK(visione::crypto::constantTimeEquals(digest, digest)); CHECK(visione::crypto::constantTimeEquals(digest, digest));
CHECK(!visione::crypto::constantTimeEquals(digest, "different")); CHECK(!visione::crypto::constantTimeEquals(digest, "different"));
} }