feat: switch email delivery from Resend to SMTP with libcurl integration
This commit is contained in:
+11
-3
@@ -5,8 +5,16 @@ 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=resend
|
|
||||||
RESEND_API_BASE_URL=https://api.resend.com
|
MAIL_PROVIDER=smtp
|
||||||
RESEND_API_KEY=
|
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]
|
MAIL_FROM=[email protected]
|
||||||
CORS_ALLOWED_ORIGIN=http://localhost:3000
|
CORS_ALLOWED_ORIGIN=http://localhost:3000
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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)
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ 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
@@ -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 \
|
||||||
libssl-dev zlib1g-dev \
|
libcurl4-openssl-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 \
|
||||||
libssl3 ca-certificates libstdc++6 libmariadb3 libjsoncpp25 libuuid1 \
|
libcurl4 libssl3 ca-certificates libstdc++6 libmariadb3 libjsoncpp25 libuuid1 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -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 \
|
||||||
libjsoncpp-dev uuid-dev zlib1g-dev libssl-dev \
|
libcurl4-openssl-dev libjsoncpp-dev uuid-dev zlib1g-dev libssl-dev \
|
||||||
libmariadb-dev libmariadb-dev-compat
|
libmariadb-dev libmariadb-dev-compat
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -95,18 +95,29 @@ The app serves the generated JSON at `/api/v1/openapi.json` and Swagger UI at
|
|||||||
|
|
||||||
## Email Delivery
|
## Email Delivery
|
||||||
|
|
||||||
OTP delivery uses Resend HTTP API through Drogon's async `HttpClient`.
|
OTP delivery uses SMTP through `libcurl`. Local dev defaults to Mailpit.
|
||||||
Set these environment variables before starting backend:
|
Set these environment variables before starting backend for local Mailpit:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export MAIL_PROVIDER=resend
|
export MAIL_PROVIDER=smtp
|
||||||
export RESEND_API_BASE_URL=https://api.resend.com
|
export SMTP_HOST=127.0.0.1
|
||||||
export RESEND_API_KEY=re_xxx
|
export SMTP_PORT=1025
|
||||||
export MAIL_FROM=[email protected]
|
export MAIL_FROM=[email protected]
|
||||||
```
|
```
|
||||||
|
|
||||||
Mailpit can still run locally for manual inspection or future integrations, but
|
For Resend in production, use SMTP credentials instead:
|
||||||
current backend OTP flow no longer speaks SMTP directly.
|
|
||||||
|
```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
|
||||||
|
|
||||||
@@ -157,7 +168,7 @@ 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`,
|
||||||
`RESEND_API_KEY`, `MAIL_FROM` 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`.
|
||||||
|
|||||||
+205
-40
@@ -1,74 +1,239 @@
|
|||||||
#include "common/Email.h"
|
#include "common/Email.h"
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
#include <drogon/drogon.h>
|
#include <drogon/drogon.h>
|
||||||
|
|
||||||
#include <json/json.h>
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
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 = drogon::app().getCustomConfig()["mail"];
|
const auto &mail = mailConfig();
|
||||||
return mail.get(key, fallback).asString();
|
return mail.get(key, fallback).asString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int configInt(const std::string &key, int fallback)
|
||||||
|
{
|
||||||
|
const auto &mail = mailConfig();
|
||||||
|
return mail.get(key, fallback).asInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool configBool(const std::string &key, bool fallback)
|
||||||
|
{
|
||||||
|
const auto &mail = mailConfig();
|
||||||
|
return mail.get(key, fallback).asBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SmtpDefaults
|
||||||
|
{
|
||||||
|
std::string host;
|
||||||
|
int port;
|
||||||
|
std::string username;
|
||||||
|
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 {
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
std::string EmailService::deliveryName() const
|
||||||
{
|
{
|
||||||
return configString("provider", "resend");
|
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 apiBaseUrl = configString("api_base_url", "https://api.resend.com");
|
const auto provider = deliveryName();
|
||||||
const auto apiKey = configString("api_key", "");
|
const auto defaults = smtpDefaults(provider);
|
||||||
|
const auto host = configString("host", defaults.host);
|
||||||
|
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 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 (apiKey.empty() || from.empty())
|
if (host.empty() || from.empty())
|
||||||
{
|
{
|
||||||
LOG_ERROR << "Email delivery configuration missing: api_key or from";
|
LOG_ERROR << "Email delivery configuration missing: host or from";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (provider == "resend" && password.empty())
|
||||||
|
{
|
||||||
|
LOG_ERROR << "Email delivery configuration missing: SMTP password for Resend";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto client = drogon::HttpClient::newHttpClient(apiBaseUrl);
|
if (!ensureCurlInitialized())
|
||||||
|
|
||||||
Json::Value payload;
|
|
||||||
payload["from"] = "Visione <" + from + ">";
|
|
||||||
payload["subject"] = "Codice OTP Visione";
|
|
||||||
payload["text"] = "Il tuo codice OTP per Visione e': " + code +
|
|
||||||
"\nScade tra 10 minuti.\n";
|
|
||||||
payload["to"] = Json::arrayValue;
|
|
||||||
payload["to"].append(to);
|
|
||||||
|
|
||||||
auto request = drogon::HttpRequest::newHttpJsonRequest(payload);
|
|
||||||
request->setMethod(drogon::Post);
|
|
||||||
request->setPath("/emails");
|
|
||||||
request->addHeader("Authorization", "Bearer " + apiKey);
|
|
||||||
request->addHeader("User-Agent", "visione-backend/1.0");
|
|
||||||
|
|
||||||
client->sendRequest(
|
|
||||||
request,
|
|
||||||
[recipient = to](drogon::ReqResult result, const drogon::HttpResponsePtr &response) {
|
|
||||||
if (result == drogon::ReqResult::Ok &&
|
|
||||||
response != nullptr &&
|
|
||||||
response->statusCode() == drogon::k200OK)
|
|
||||||
{
|
{
|
||||||
LOG_INFO << "OTP email queued via Resend for " << recipient;
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto body = response != nullptr ? response->getBody() : std::string{};
|
std::ostringstream payload;
|
||||||
LOG_ERROR << "OTP email delivery failed via Resend for "
|
payload << "To: <" << to << ">\r\n";
|
||||||
<< recipient
|
payload << "From: Visione <" << from << ">\r\n";
|
||||||
<< ", result=" << static_cast<int>(result)
|
payload << "Subject: Codice OTP Visione\r\n";
|
||||||
<< ", status="
|
payload << "MIME-Version: 1.0\r\n";
|
||||||
<< (response != nullptr ? std::to_string(response->statusCode())
|
payload << "Content-Type: text/plain; charset=utf-8\r\n";
|
||||||
: "none")
|
payload << "\r\n";
|
||||||
<< ", body=" << body;
|
payload << "Il tuo codice OTP per Visione e': " << code << "\r\n";
|
||||||
});
|
payload << "Scade tra 10 minuti.\r\n";
|
||||||
|
const auto payloadString = payload.str();
|
||||||
|
const auto url = smtpUrl(host, port, useTls);
|
||||||
|
const auto mailFrom = "<" + from + ">";
|
||||||
|
const auto recipientAddress = "<" + to + ">";
|
||||||
|
|
||||||
|
using CurlHandle = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurlList recipients(nullptr, &curl_slist_free_all);
|
||||||
|
recipients.reset(curl_slist_append(nullptr, recipientAddress.c_str()));
|
||||||
|
if (!recipients)
|
||||||
|
{
|
||||||
|
LOG_ERROR << "Could not build SMTP recipient list for " << to;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UploadContext uploadContext{.payload = &payloadString};
|
||||||
|
char errorBuffer[CURL_ERROR_SIZE] = {0};
|
||||||
|
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_MAIL_FROM, mailFrom.c_str());
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_MAIL_RCPT, recipients.get());
|
||||||
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
} // namespace visione::email
|
} // namespace visione::email
|
||||||
|
|||||||
+10
-4
@@ -64,10 +64,16 @@
|
|||||||
"default_frontend_redirect_uri": "http://localhost:3000/auth/cie/callback"
|
"default_frontend_redirect_uri": "http://localhost:3000/auth/cie/callback"
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"provider": "resend",
|
"provider": "smtp",
|
||||||
"api_base_url": "https://api.resend.com",
|
"host": "127.0.0.1",
|
||||||
"api_key": "",
|
"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",
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ services:
|
|||||||
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}
|
MAIL_PROVIDER: ${MAIL_PROVIDER:-resend}
|
||||||
RESEND_API_BASE_URL: ${RESEND_API_BASE_URL:-https://api.resend.com}
|
SMTP_HOST: ${SMTP_HOST:-smtp.resend.com}
|
||||||
|
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:-}
|
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
||||||
MAIL_FROM: ${MAIL_FROM:[email protected]}
|
MAIL_FROM: ${MAIL_FROM:[email protected]}
|
||||||
CORS_ALLOWED_ORIGIN: ${CORS_ALLOWED_ORIGIN:-http://localhost:3000}
|
CORS_ALLOWED_ORIGIN: ${CORS_ALLOWED_ORIGIN:-http://localhost:3000}
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ Authorization: Bearer <token>
|
|||||||
- `POST /auth/register`
|
- `POST /auth/register`
|
||||||
citizen registration without password
|
citizen registration without password
|
||||||
- `POST /auth/otp/request`
|
- `POST /auth/otp/request`
|
||||||
queues OTP email through configured mail provider
|
sends OTP email through configured SMTP provider
|
||||||
- `POST /auth/otp/verify`
|
- `POST /auth/otp/verify`
|
||||||
consumes OTP, returns bearer token
|
consumes OTP, returns bearer token
|
||||||
- `POST /auth/cie/start`
|
- `POST /auth/cie/start`
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ generate public read-only intervention links.
|
|||||||
|
|
||||||
## Runtime Model
|
## Runtime Model
|
||||||
- Auth uses bearer tokens from `api_tokens`.
|
- Auth uses bearer tokens from `api_tokens`.
|
||||||
- OTP login uses `otp_challenges` and async email delivery through configured mail provider.
|
- OTP login uses `otp_challenges` and SMTP email delivery through configured mail provider.
|
||||||
- CIE login uses external Django RP service:
|
- CIE login uses external Django RP service:
|
||||||
- Visione creates local `state`
|
- Visione creates local `state`
|
||||||
- Django RP completes CIE/OIDC and sends signed identity payload to Visione
|
- Django RP completes CIE/OIDC and sends signed identity payload to Visione
|
||||||
|
|||||||
@@ -2582,7 +2582,7 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/auth/otp/request": {
|
"/api/v1/auth/otp/request": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Queues a one-time code by email through configured mail provider.",
|
"description": "Sends a one-time code by email through configured SMTP provider.",
|
||||||
"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.",
|
"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.",
|
||||||
"operationId": "ReportController.list",
|
"operationId": "ReportController.list",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,8 +5,24 @@
|
|||||||
#include "repositories/MigrationRunner.h"
|
#include "repositories/MigrationRunner.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <fstream>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
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()
|
||||||
{
|
{
|
||||||
@@ -19,6 +35,7 @@ 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"))
|
||||||
@@ -32,16 +49,35 @@ 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"))
|
if (const char* envMailProvider = std::getenv("MAIL_PROVIDER"))
|
||||||
config["custom_config"]["mail"]["provider"] = envMailProvider;
|
config["custom_config"]["mail"]["provider"] = envMailProvider;
|
||||||
if (const char* envMailApiBaseUrl = std::getenv("RESEND_API_BASE_URL"))
|
if (const char* envSmtpHost = std::getenv("SMTP_HOST"))
|
||||||
config["custom_config"]["mail"]["api_base_url"] = envMailApiBaseUrl;
|
config["custom_config"]["mail"]["host"] = envSmtpHost;
|
||||||
if (const char* envMailApiKey = std::getenv("RESEND_API_KEY"))
|
if (const char* envSmtpPort = std::getenv("SMTP_PORT"))
|
||||||
config["custom_config"]["mail"]["api_key"] = envMailApiKey;
|
config["custom_config"]["mail"]["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"))
|
if (const char* envMailFrom = std::getenv("MAIL_FROM"))
|
||||||
config["custom_config"]["mail"]["from"] = envMailFrom;
|
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"))
|
if (const char* envCieServiceBaseUrl = std::getenv("CIE_AUTH_SERVICE_BASE_URL"))
|
||||||
config["custom_config"]["cie_auth"]["service_base_url"] = envCieServiceBaseUrl;
|
config["custom_config"]["cie_auth"]["service_base_url"] = envCieServiceBaseUrl;
|
||||||
if (const char* envCieStartPath = std::getenv("CIE_AUTH_START_PATH"))
|
if (const char* envCieStartPath = std::getenv("CIE_AUTH_START_PATH"))
|
||||||
@@ -52,6 +88,7 @@ int main()
|
|||||||
config["custom_config"]["cie_auth"]["state_ttl_minutes"] = std::stoi(envCieStateTtl);
|
config["custom_config"]["cie_auth"]["state_ttl_minutes"] = std::stoi(envCieStateTtl);
|
||||||
if (const char* envCieFrontendRedirect = std::getenv("CIE_AUTH_DEFAULT_FRONTEND_REDIRECT_URI"))
|
if (const char* envCieFrontendRedirect = std::getenv("CIE_AUTH_DEFAULT_FRONTEND_REDIRECT_URI"))
|
||||||
config["custom_config"]["cie_auth"]["default_frontend_redirect_uri"] = envCieFrontendRedirect;
|
config["custom_config"]["cie_auth"]["default_frontend_redirect_uri"] = envCieFrontendRedirect;
|
||||||
|
|
||||||
drogon::app().loadConfigJson(config);
|
drogon::app().loadConfigJson(config);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ 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 queue OTP email through " << mail.deliveryName();
|
LOG_WARN << "Could not send OTP email through " << mail.deliveryName();
|
||||||
return failure("otp_email_failed");
|
return failure("otp_email_failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user