1. Introducción
Este manual detalla cómo crear una REST API en C++ utilizando Boost para gestionar una clínica médica. La API incluirá:
- Autenticación con JWT (JSON Web Tokens)
- Hash de contraseñas con Bcrypt
- Persistencia de datos en RethinkDB
- Caché de rutas con Valkey (fork moderno de Redis)
- Operaciones CRUD para pacientes, doctores y citas
Nota: Este manual asume que tienes conocimientos intermedios de C++ y familiaridad con conceptos de desarrollo web.
2. Configuración del proyecto
Primero, necesitamos configurar nuestro entorno de desarrollo:
Requisitos
- C++17 o superior
- Boost 1.75 o superior
- RethinkDB 2.4 o superior
- Valkey (o Redis) 7.0 o superior
- CMake 3.15 o superior
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(ClinicRestAPI VERSION 1.0.0 LANGUAGES CXX)
# Configuración estándar de C++
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Buscar paquetes requeridos
find_package(Boost 1.75.0 REQUIRED COMPONENTS
beast
system
json
random
url
nowide
program_options)
# Configuración de dependencias
find_package(RethinkDB REQUIRED)
find_package(Valkey REQUIRED)
find_package(OpenSSL REQUIRED)
# Ejecutable principal
add_executable(clinic_api
src/main.cpp
src/server.cpp
src/database.cpp
src/auth.cpp
src/cache.cpp
src/models.cpp
src/routes.cpp
)
# Vincular dependencias
target_link_libraries(clinic_api
PRIVATE
Boost::beast
Boost::system
Boost::json
Boost::random
Boost::url
Boost::nowide
Boost::program_options
RethinkDB::RethinkDB
Valkey::Valkey
OpenSSL::SSL
OpenSSL::Crypto
)
3. Estructura del proyecto
Organizaremos el proyecto de la siguiente manera:
clinic_api/ ├── CMakeLists.txt ├── include/ │ ├── server.hpp │ ├── database.hpp │ ├── auth.hpp │ ├── cache.hpp │ ├── models.hpp │ └── routes.hpp ├── src/ │ ├── main.cpp │ ├── server.cpp │ ├── database.cpp │ ├── auth.cpp │ ├── cache.cpp │ ├── models.cpp │ └── routes.cpp ├── tests/ └── config/ ├── config.json └── jwt_keys/
Archivos principales
Archivo | Propósito |
---|---|
server.hpp/cpp | Configuración del servidor HTTP con Boost.Beast |
database.hpp/cpp | Conexión y operaciones con RethinkDB |
auth.hpp/cpp | Autenticación con JWT y hash de contraseñas |
cache.hpp/cpp | Integración con Valkey para caché |
models.hpp/cpp | Modelos de datos (Paciente, Doctor, Cita) |
routes.hpp/cpp | Definición de rutas y handlers |
4. Boost.Beast para HTTP
Boost.Beast es una biblioteca para implementar protocolos web (HTTP/WebSocket) sobre Boost.Asio.
Configuración básica del servidor
// server.hpp
#pragma once
#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <memory>
#include <string>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
class HTTPServer {
public:
HTTPServer(std::string address, uint16_t port);
~HTTPServer();
void run();
void stop();
private:
void start_accept();
void handle_accept(beast::error_code ec, tcp::socket socket);
net::io_context ioc_;
tcp::acceptor acceptor_;
std::atomic<bool> stopped_{false};
};
Implementación del servidor
// server.cpp
#include "server.hpp"
#include <iostream>
HTTPServer::HTTPServer(std::string address, uint16_t port)
: ioc_(1), acceptor_(ioc_) {
beast::error_code ec;
tcp::endpoint endpoint(net::ip::make_address(address), port);
// Abrir el acceptor
acceptor_.open(endpoint.protocol(), ec);
if (ec) {
throw beast::system_error{ec};
}
// Permitir reutilizar la dirección
acceptor_.set_option(net::socket_base::reuse_address(true), ec);
if (ec) {
throw beast::system_error{ec};
}
// Vincular a la dirección y puerto
acceptor_.bind(endpoint, ec);
if (ec) {
throw beast::system_error{ec};
}
// Empezar a escuchar
acceptor_.listen(net::socket_base::max_listen_connections, ec);
if (ec) {
throw beast::system_error{ec};
}
}
void HTTPServer::run() {
start_accept();
ioc_.run();
}
void HTTPServer::start_accept() {
acceptor_.async_accept(
[this](beast::error_code ec, tcp::socket socket) {
handle_accept(ec, std::move(socket));
});
}
void HTTPServer::handle_accept(beast::error_code ec, tcp::socket socket) {
if (ec) {
std::cerr << "Error en accept: " << ec.message() << "\n";
} else {
// Crear una nueva sesión para manejar la conexión
// (implementaremos esto más adelante)
// std::make_shared<Session>(std::move(socket))->start();
}
if (!stopped_) {
start_accept();
}
}
void HTTPServer::stop() {
stopped_ = true;
ioc_.stop();
}
5. Implementación de JWT
JSON Web Tokens (JWT) nos permitirá manejar autenticación de manera segura y escalable.
Generación y verificación de tokens
// auth.hpp
#pragma once
#include <boost/json.hpp>
#include <string>
#include <chrono>
namespace json = boost::json;
class JWT {
public:
JWT(std::string secret_key, std::string algorithm = "HS256");
std::string generate(const json::object& payload,
std::chrono::seconds expires_in = std::chrono::hours(1));
bool verify(const std::string& token);
json::value decode(const std::string& token);
private:
std::string sign(const std::string& header, const std::string& payload);
std::string secret_key_;
std::string algorithm_;
};
Implementación de JWT
// auth.cpp - Implementación parcial
#include "auth.hpp"
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <boost/algorithm/string.hpp>
#include <boost/beast/core/detail/base64.hpp>
std::string JWT::generate(const json::object& payload,
std::chrono::seconds expires_in) {
// Crear el header
json::object header;
header["alg"] = algorithm_;
header["typ"] = "JWT";
// Crear una copia del payload y añadir timestamps
json::object payload_copy = payload;
auto now = std::chrono::system_clock::now();
auto exp = now + expires_in;
payload_copy["iat"] = std::chrono::duration_cast<std::chrono::seconds>(
now.time_since_epoch()).count();
payload_copy["exp"] = std::chrono::duration_cast<std::chrono::seconds>(
exp.time_since_epoch()).count();
// Codificar header y payload
std::string header_str = json::serialize(header);
std::string payload_str = json::serialize(payload_copy);
std::string encoded_header;
std::string encoded_payload;
// Codificación Base64URL
size_t encode_len = beast::detail::base64::encoded_size(header_str.size());
encoded_header.resize(encode_len);
beast::detail::base64::encode(encoded_header.data(), header_str.data(), header_str.size());
encode_len = beast::detail::base64::encoded_size(payload_str.size());
encoded_payload.resize(encode_len);
beast::detail::base64::encode(encoded_payload.data(), payload_str.data(), payload_str.size());
// Eliminar padding y reemplazar caracteres para Base64URL
boost::algorithm::erase_all(encoded_header, "=");
boost::algorithm::replace_all(encoded_header, "+", "-");
boost::algorithm::replace_all(encoded_header, "/", "_");
boost::algorithm::erase_all(encoded_payload, "=");
boost::algorithm::replace_all(encoded_payload, "+", "-");
boost::algorithm::replace_all(encoded_payload, "/", "_");
// Crear la firma
std::string signature = sign(encoded_header, encoded_payload);
// Construir el token JWT
return encoded_header + "." + encoded_payload + "." + signature;
}
bool JWT::verify(const std::string& token) {
// 1. Dividir el token en sus partes
std::vector<std::string> parts;
boost::algorithm::split(parts, token, boost::is_any_of("."));
if (parts.size() != 3) {
return false;
}
// 2. Verificar la firma
std::string signature = sign(parts[0], parts[1]);
// Comparar firmas (a prueba de timing attacks)
bool sig_match = boost::algorithm::equals(signature, parts[2]);
if (!sig_match) {
return false;
}
// 3. Verificar expiración
auto payload = decode(token);
auto& obj = payload.as_object();
if (!obj.contains("exp")) {
return false;
}
auto now = std::chrono::system_clock::now();
auto exp_time = std::chrono::system_clock::time_point(
std::chrono::seconds(obj["exp"].as_int64()));
return now < exp_time;
}
6. Hash de contraseñas con Bcrypt
Para almacenar contraseñas de manera segura, usaremos el algoritmo bcrypt.
Implementación de Bcrypt
// auth.hpp - Añadir esta clase
class PasswordHasher {
public:
PasswordHasher(int work_factor = 12);
std::string hash(const std::string& password);
bool verify(const std::string& password, const std::string& hash);
private:
int work_factor_;
};
Uso de OpenSSL para Bcrypt
// auth.cpp - Implementación de PasswordHasher
#include <openssl/evp.h>
#include <openssl/rand.h>
std::string PasswordHasher::hash(const std::string& password) {
// Generar salt
unsigned char salt[16];
if (RAND_bytes(salt, sizeof(salt)) != 1) {
throw std::runtime_error("Error generando salt");
}
// Generar hash bcrypt
char bcrypt_hash[61]; // 60 caracteres + null terminator
if (!bcrypt_hashpw(password.c_str(), salt, bcrypt_hash)) {
throw std::runtime_error("Error generando hash bcrypt");
}
return std::string(bcrypt_hash);
}
bool PasswordHasher::verify(const std::string& password, const std::string& hash) {
// Verificar contraseña con bcrypt
return bcrypt_checkpw(password.c_str(), hash.c_str()) == 0;
}
Importante: Nunca almacenes contraseñas en texto plano. Siempre usa un algoritmo de hash seguro como bcrypt que incluye salt y es computacionalmente costoso.
7. Conexión con RethinkDB
RethinkDB es una base de datos NoSQL con capacidades de cambio en tiempo real.
Conexión y manejo básico
// database.hpp
#pragma once
#include <rethinkdb.h>
#include <boost/json.hpp>
#include <string>
#include <memory>
namespace json = boost::json;
class Database {
public:
Database(const std::string& host, int port,
const std::string& db_name);
// Operaciones CRUD para pacientes
json::value create_patient(const json::object& patient_data);
json::value get_patient(const std::string& patient_id);
json::value update_patient(const std::string& patient_id,
const json::object& update_data);
bool delete_patient(const std::string& patient_id);
// Operaciones similares para doctores y citas...
private:
std::shared_ptr<rethinkdb::connection> conn_;
std::string db_name_;
void ensure_tables();
};
Implementación de la conexión
// database.cpp
#include "database.hpp"
#include <stdexcept>
Database::Database(const std::string& host, int port,
const std::string& db_name)
: db_name_(db_name) {
// Crear opciones de conexión
rethinkdb::connection::options opts;
opts.host = host;
opts.port = port;
// Establecer conexión
conn_ = std::make_shared<rethinkdb::connection>(opts);
try {
// Verificar si la base de datos existe, si no, crearla
auto cursor = rethinkdb::db_list().run(*conn_);
bool db_exists = false;
for (const auto& db : cursor) {
if (db.as_string() == db_name) {
db_exists = true;
break;
}
}
if (!db_exists) {
rethinkdb::db_create(db_name).run(*conn_);
}
// Asegurar que las tablas existan
ensure_tables();
} catch (const std::exception& e) {
throw std::runtime_error("Error inicializando base de datos: " + std::string(e.what()));
}
}
void Database::ensure_tables() {
const std::vector<std::string> tables = {
"patients", "doctors", "appointments", "users"
};
auto cursor = rethinkdb::db(db_name_).table_list().run(*conn_);
std::unordered_set<std::string> existing_tables;
for (const auto& table : cursor) {
existing_tables.insert(table.as_string());
}
for (const auto& table : tables) {
if (existing_tables.find(table) == existing_tables.end()) {
rethinkdb::db(db_name_).table_create(table).run(*conn_);
// Crear índices para búsquedas comunes
if (table == "patients") {
rethinkdb::db(db_name_).table(table)
.index_create("email").run(*conn_);
} else if (table == "appointments") {
rethinkdb::db(db_name_).table(table)
.index_create("patient_id").run(*conn_);
rethinkdb::db(db_name_).table(table)
.index_create("doctor_id").run(*conn_);
rethinkdb::db(db_name_).table(table)
.index_create("date").run(*conn_);
}
}
}
}
// Ejemplo de implementación de CRUD para pacientes
json::value Database::create_patient(const json::object& patient_data) {
auto result = rethinkdb::db(db_name_)
.table("patients")
.insert(patient_data)
.run(*conn_);
return json::parse(result.to_json());
}
json::value Database::get_patient(const std::string& patient_id) {
auto cursor = rethinkdb::db(db_name_)
.table("patients")
.get(patient_id)
.run(*conn_);
if (cursor.is_empty()) {
throw std::runtime_error("Paciente no encontrado");
}
return json::parse(cursor.to_json());
}
8. Cache con Valkey (Redis)
Valkey es un fork moderno de Redis que usaremos para cachear respuestas y mejorar el rendimiento.
Cliente Valkey
// cache.hpp
#pragma once
#include <valkey.h>
#include <boost/json.hpp>
#include <string>
#include <memory>
namespace json = boost::json;
class Cache {
public:
Cache(const std::string& host, int port);
// Operaciones básicas
void set(const std::string& key, const std::string& value,
std::chrono::seconds ttl = std::chrono::seconds(0));
std::string get(const std::string& key);
bool del(const std::string& key);
bool exists(const std::string& key);
// Operaciones específicas para la API
void cache_response(const std::string& route, const json::value& response,
std::chrono::seconds ttl = std::chrono::minutes(5));
json::value get_cached_response(const std::string& route);
private:
std::unique_ptr<valkey::client> client_;
};
Implementación del caché
// cache.cpp
#include "cache.hpp"
#include <stdexcept>
Cache::Cache(const std::string& host, int port) {
valkey::connection_options opts;
opts.host = host;
opts.port = port;
try {
client_ = std::make_unique<valkey::client>(opts);
// Verificar conexión
client_->ping();
} catch (const std::exception& e) {
throw std::runtime_error("Error conectando a Valkey: " + std::string(e.what()));
}
}
void Cache::set(const std::string& key, const std::string& value,
std::chrono::seconds ttl) {
if (ttl.count() > 0) {
client_->setex(key, static_cast<long long>(ttl.count()), value);
} else {
client_->set(key, value);
}
}
std::string Cache::get(const std::string& key) {
auto reply = client_->get(key);
if (reply.is_null()) {
return "";
}
return reply.as_string();
}
void Cache::cache_response(const std::string& route, const json::value& response,
std::chrono::seconds ttl) {
std::string key = "route:" + route;
std::string value = json::serialize(response);
set(key, value, ttl);
}
json::value Cache::get_cached_response(const std::string& route) {
std::string key = "route:" + route;
std::string value = get(key);
if (value.empty()) {
throw std::runtime_error("Respuesta no encontrada en caché");
}
return json::parse(value);
}
Middleware de caché
Podemos crear un middleware que verifique el caché antes de procesar una solicitud:
// server.cpp - Añadir esta función
template <typename Handler>
auto make_cached_handler(Cache& cache, Handler handler) {
return [&cache, handler](const http::request<http::string_body>& req) {
std::string cache_key = std::string(req.method_string()) +
":" + std::string(req.target());
try {
// Intentar obtener del caché
auto cached_response = cache.get_cached_response(cache_key);
http::response<http::string_body> res{http::status::ok, req.version()};
res.set(http::field::content_type, "application/json");
res.body() = json::serialize(cached_response);
res.prepare_payload();
return res;
} catch (const std::exception&) {
// No está en caché, procesar normalmente
auto response = handler(req);
// Almacenar en caché si fue exitoso
if (response.result() == http::status::ok) {
auto body = json::parse(response.body());
cache.cache_response(cache_key, body);
}
return response;
}
};
}
9. Implementación del CRUD
Implementaremos las operaciones CRUD para pacientes, doctores y citas.
Modelos de datos
// models.hpp
#pragma once
#include <boost/json.hpp>
#include <string>
#include <chrono>
namespace json = boost::json;
struct Patient {
std::string id;
std::string name;
std::string email;
std::string phone;
std::string address;
std::string birth_date;
std::string blood_type;
Patient() = default;
Patient(const json::object& obj);
json::object to_json() const;
};
struct Doctor {
std::string id;
std::string name;
std::string email;
std::string phone;
std::string specialization;
std::string license_number;
Doctor() = default;
Doctor(const json::object& obj);
json::object to_json() const;
};
struct Appointment {
std::string id;
std::string patient_id;
std::string doctor_id;
std::string date;
std::string time;
std::string status; // scheduled, completed, canceled
std::string notes;
Appointment() = default;
Appointment(const json::object& obj);
json::object to_json() const;
};
Implementación de los modelos
// models.cpp
#include "models.hpp"
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
Patient::Patient(const json::object& obj) {
if (obj.contains("id")) {
id = obj.at("id").as_string().c_str();
} else {
// Generar un nuevo UUID si no se proporciona
id = boost::uuids::to_string(boost::uuids::random_generator()());
}
name = obj.at("name").as_string().c_str();
email = obj.at("email").as_string().c_str();
phone = obj.at("phone").as_string().c_str();
address = obj.at("address").as_string().c_str();
birth_date = obj.at("birth_date").as_string().c_str();
blood_type = obj.at("blood_type").as_string().c_str();
}
json::object Patient::to_json() const {
return {
{"id", id},
{"name", name},
{"email", email},
{"phone", phone},
{"address", address},
{"birth_date", birth_date},
{"blood_type", blood_type}
};
}
// Implementaciones similares para Doctor y Appointment...
Handlers para el CRUD
// routes.cpp - Ejemplo de handlers
#include "routes.hpp"
#include "models.hpp"
#include "auth.hpp"
#include "database.hpp"
#include <boost/beast/http.hpp>
namespace http = boost::beast::http;
http::response<http::string_body>
handle_create_patient(const http::request<http::string_body>& req,
Database& db, JWT& jwt) {
// Verificar autenticación
auto auth_header = req.find(http::field::authorization);
if (auth_header == req.end()) {
return create_unauthorized_response(req, "Falta token de autenticación");
}
std::string token = std::string(auth_header->value());
if (!jwt.verify(token)) {
return create_unauthorized_response(req, "Token inválido o expirado");
}
// Parsear cuerpo de la solicitud
auto patient_data = json::parse(req.body()).as_object();
Patient patient(patient_data);
// Insertar en la base de datos
auto result = db.create_patient(patient.to_json());
// Construir respuesta
http::response<http::string_body> res{http::status::created, req.version()};
res.set(http::field::content_type, "application/json");
res.body() = json::serialize(result);
res.prepare_payload();
return res;
}
http::response<http::string_body>
handle_get_patient(const http::request<http::string_body>& req,
Database& db, JWT& jwt, Cache& cache) {
// Extraer ID del paciente de la URL
std::string patient_id = std::string(req.target()).substr(10); // "/patients/123"
try {
// Obtener paciente de la base de datos
auto patient = db.get_patient(patient_id);
// Construir respuesta
http::response<http::string_body> res{http::status::ok, req.version()};
res.set(http::field::content_type, "application/json");
res.body() = json::serialize(patient);
res.prepare_payload();
return res;
} catch (const std::exception& e) {
return create_error_response(req, http::status::not_found, e.what());
}
}
// Funciones similares para update_patient, delete_patient, y operaciones para doctores y citas...
10. Definición de rutas
Definiremos las rutas de nuestra API y las conectaremos con los handlers.
Tabla de rutas
Método | Ruta | Descripción |
---|---|---|
POST | /api/auth/register | Registrar un nuevo usuario |
POST | /api/auth/login | Iniciar sesión y obtener JWT |
GET | /api/patients | Listar pacientes (paginated) |
POST | /api/patients | Crear un nuevo paciente |
GET | /api/patients/{id} | Obtener un paciente específico |
PUT | /api/patients/{id} | Actualizar un paciente |
DELETE | /api/patients/{id} | Eliminar un paciente |
GET | /api/doctors | Listar doctores |
GET | /api/appointments | Listar citas (con filtros) |
POST | /api/appointments | Crear una nueva cita |
Router básico
// routes.hpp
#pragma once
#include <boost/beast/http.hpp>
#include <functional>
#include <unordered_map>
#include <memory>
namespace http = boost::beast::http;
using Request = http::request<http::string_body>;
using Response = http::response<http::string_body>;
using Handler = std::function<Response(const Request&)>;
class Router {
public:
Router() = default;
void add_route(http::verb method, const std::string& path, Handler handler);
Response handle_request(const Request& req);
private:
std::unordered_map<std::string,
std::unordered_map<http::verb, Handler>> routes_;
};
Implementación del router
// routes.cpp
#include "routes.hpp"
#include <boost/algorithm/string.hpp>
void Router::add_route(http::verb method, const std::string& path, Handler handler) {
routes_[path][method] = std::move(handler);
}
Response Router::handle_request(const Request& req) {
std::string path = std::string(req.target());
// Buscar coincidencia exacta primero
if (routes_.find(path) != routes_.end()) {
auto& methods = routes_.at(path);
if (methods.find(req.method()) != methods.end()) {
return methods.at(req.method())(req);
}
}
// Buscar rutas con parámetros (como /patients/:id)
for (const auto& [route_path, methods] : routes_) {
if (route_path.find(":") != std::string::npos) {
std::vector<std::string> route_parts;
boost::algorithm::split(route_parts, route_path, boost::is_any_of("/"));
std::vector<std::string> path_parts;
boost::algorithm::split(path_parts, path, boost::is_any_of("/"));
if (route_parts.size() == path_parts.size()) {
bool match = true;
for (size_t i = 0; i < route_parts.size(); ++i) {
if (route_parts[i] != path_parts[i] && !route_parts[i].starts_with(":")) {
match = false;
break;
}
}
if (match && methods.find(req.method()) != methods.end()) {
return methods.at(req.method())(req);
}
}
}
}
// No se encontró la ruta
return create_error_response(req, http::status::not_found, "Ruta no encontrada");
}
Configuración de rutas en main.cpp
// main.cpp - Configuración de rutas
#include "server.hpp"
#include "database.hpp"
#include "auth.hpp"
#include "cache.hpp"
#include "routes.hpp"
#include <boost/program_options.hpp>
int main(int argc, char* argv[]) {
// Configuración inicial...
// Inicializar componentes
Database db(config.db_host, config.db_port, config.db_name);
Cache cache(config.redis_host, config.redis_port);
JWT jwt(config.jwt_secret);
PasswordHasher hasher();
// Configurar rutas
Router router;
// Autenticación
router.add_route(http::verb::post, "/api/auth/register",
[&](const auto& req) { return handle_register(req, db, hasher, jwt); });
router.add_route(http::verb::post, "/api/auth/login",
[&](const auto& req) { return handle_login(req, db, hasher, jwt); });
// Pacientes
router.add_route(http::verb::get, "/api/patients",
make_cached_handler(cache,
[&](const auto& req) { return handle_list_patients(req, db, jwt); }));
router.add_route(http::verb::post, "/api/patients",
[&](const auto& req) { return handle_create_patient(req, db, jwt); });
router.add_route(http::verb::get, "/api/patients/:id",
make_cached_handler(cache,
[&](const auto& req) { return handle_get_patient(req, db, jwt, cache); }));
// Iniciar servidor
HTTPServer server(config.address, config.port);
server.set_request_handler([&router](const auto& req) {
return router.handle_request(req);
});
server.run();
return 0;
}
11. Consideraciones de seguridad
Implementaremos varias medidas de seguridad para proteger nuestra API.
Protecciones básicas
- HTTPS: Siempre usar HTTPS en producción.
- CORS: Configurar adecuadamente los headers CORS.
- Rate limiting: Limitar peticiones para prevenir ataques de fuerza bruta.
- Validación de entrada: Validar todos los datos de entrada.
Middleware de seguridad
// server.cpp - Middleware de seguridad
http::response<http::string_body>
add_security_headers(http::response<http::string_body> res) {
// Headers de seguridad básicos
res.set(http::field::strict_transport_security, "max-age=63072000; includeSubDomains; preload");
res.set(http::field::x_content_type_options, "nosniff");
res.set(http::field::x_frame_options, "DENY");
res.set(http::field::x_xss_protection, "1; mode=block");
res.set(http::field::content_security_policy,
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'");
// Headers CORS (ajustar según necesidades)
res.set(http::field::access_control_allow_origin, "*");
res.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE, OPTIONS");
res.set(http::field::access_control_allow_headers,
"Content-Type, Authorization");
return res;
}
Validación de entrada
// auth.cpp - Validación de registro
void validate_registration(const json::object& data) {
static const std::regex email_regex(
R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)");
if (!data.contains("email") || !data.contains("password")) {
throw std::invalid_argument("Email y password son requeridos");
}
std::string email = data.at("email").as_string().c_str();
std::string password = data.at("password").as_string().c_str();
if (!std::regex_match(email, email_regex)) {
throw std::invalid_argument("Email inválido");
}
if (password.length() < 8) {
throw std::invalid_argument("Password debe tener al menos 8 caracteres");
}
}
12. Despliegue
Finalmente, veremos cómo desplegar nuestra aplicación en producción.
Configuración de producción
Crear un archivo config/production.json
:
{
"address": "0.0.0.0",
"port": 8080,
"db_host": "rethinkdb-prod",
"db_port": 28015,
"db_name": "clinic_prod",
"redis_host": "valkey-prod",
"redis_port": 6379,
"jwt_secret": "SECRETO_MUY_SEGURO_AQUI",
"ssl_cert": "/etc/ssl/certs/clinic_api.crt",
"ssl_key": "/etc/ssl/private/clinic_api.key"
}
Dockerfile para producción
# Dockerfile
FROM ubuntu:22.04 AS builder
# Instalar dependencias de construcción
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
libboost-all-dev \
libssl-dev \
librethinkdb-dev \
libvalkey-dev \
git
# Copiar código fuente
COPY . /app
WORKDIR /app/build
# Construir la aplicación
RUN cmake .. && cmake --build . --config Release
# Imagen final más pequeña
FROM ubuntu:22.04
# Instalar solo dependencias de runtime
RUN apt-get update && apt-get install -y \
libboost-system1.74.0 \
libboost-json1.74.0 \
libssl3 \
librethinkdb2.4 \
libvalkey7 && \
rm -rf /var/lib/apt/lists/*
# Copiar binario y configuración
COPY --from=builder /app/build/clinic_api /usr/local/bin/
COPY config/production.json /etc/clinic_api/config.json
# Exponer puerto
EXPOSE 8080
# Comando de inicio
CMD ["clinic_api", "--config", "/etc/clinic_api/config.json"]
docker-compose.yml
# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "8080:8080"
depends_on:
- rethinkdb
- valkey
environment:
- APP_ENV=production
restart: unless-stopped
rethinkdb:
image: rethinkdb:2.4
ports:
- "28015:28015"
- "8081:8080" # Admin UI
volumes:
- rethinkdb_data:/data
restart: unless-stopped
valkey:
image: valkey/valkey:7
ports:
- "6379:6379"
volumes:
- valkey_data:/data
restart: unless-stopped
volumes:
rethinkdb_data:
valkey_data:
Nota: Para producción real, considera añadir un proxy inverso como Nginx para manejar SSL terminación, balanceo de carga y compresión.