diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a21dafa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +build/ +.git/ +.vscode/ +.cache/ +uploads/ +.env +.env.example \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..411a3a7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# --- Builder Stage --- +FROM ubuntu:22.04 AS builder +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build git pkg-config \ + libssl-dev zlib1g-dev \ + libmariadb-dev libmariadb-dev-compat \ + libsqlite3-dev python3 libjsoncpp-dev uuid-dev ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN rm -rf /usr/include/mysql && ln -s /usr/include/mariadb /usr/include/mysql + +WORKDIR /app +COPY . /app + +RUN cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + && cmake --build build + +# --- Runtime Stage --- +FROM ubuntu:22.04 AS runtime +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libssl3 ca-certificates libstdc++6 libmariadb3 libjsoncpp25 libuuid1 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY --from=builder /app/build/visione-backend /app/visione-backend +COPY --from=builder /app/build/openapi /app/build/openapi +COPY --from=builder /app/config.json /app/config.json +COPY --from=builder /app/migrations /app/migrations + +EXPOSE 5555 + +ENTRYPOINT ["/app/visione-backend"] diff --git a/README.md b/README.md index 13cda15..dd0ba13 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,20 @@ ctest --test-dir build --output-on-failure The `RequestLogger` plugin logs every response as method, path, status and client address. +## Dokploy + +Use `docker-compose.dev.yml` as the Dokploy compose path for a self-contained +backend deployment with MySQL and a local SMTP catcher. Configure the public +domain in Dokploy against service `backend` and container port `5555`. + +The runtime image includes `config.json`; override environment-specific values +with Dokploy environment variables. The SMTP catcher service keeps the internal +name `mailhog` for compatibility, but uses the multi-architecture Mailpit image +to avoid `exec format error` failures on ARM servers. + +Set `MYSQL_PASSWORD`, `MYSQL_ROOT_PASSWORD` and any other environment-specific +values in Dokploy environment variables. + ## Current Endpoints - `GET /api/v1/health`: process health check, no external dependency. diff --git a/common/Cors.cc b/common/Cors.cc index 6d1fc33..45881c5 100644 --- a/common/Cors.cc +++ b/common/Cors.cc @@ -6,33 +6,91 @@ namespace visione::cors { namespace { -std::string allowedOrigin() +std::vector allowedOrigins() { - return drogon::app() - .getCustomConfig()["cors"] - .get("allowed_origin", "http://localhost:5173") - .asString(); + auto cfg = drogon::app().getCustomConfig(); + std::vector origins; + + if (cfg.isMember("cors") && cfg["cors"].isObject()) + { + auto &c = cfg["cors"]; + if (c.isMember("allowed_origin")) + { + auto &v = c["allowed_origin"]; + if (v.isArray()) + { + for (const auto &el : v) + { + if (el.isString()) + origins.emplace_back(el.asString()); + } + } + else if (v.isString()) + { + origins.emplace_back(v.asString()); + } + } + } + + if (origins.empty()) + origins.emplace_back("http://localhost:5173"); + + return origins; } bool isAllowed(const drogon::HttpRequestPtr &request) { - return request->getHeader("Origin") == allowedOrigin(); + auto origin = request->getHeader("Origin"); + if (origin.empty()) + return false; + + auto origins = allowedOrigins(); + for (const auto &o : origins) + { + if (o == "*" || o == origin) + return true; + } + return false; } void addHeaders(const drogon::HttpRequestPtr &request, const drogon::HttpResponsePtr &response) { - if (!isAllowed(request)) - { + auto origin = request->getHeader("Origin"); + if (origin.empty()) return; + + auto origins = allowedOrigins(); + bool allowAll = false; + bool matched = false; + for (const auto &o : origins) + { + if (o == "*") + { + allowAll = true; + break; + } + if (o == origin) + { + matched = true; + break; + } } - response->addHeader("Access-Control-Allow-Origin", allowedOrigin()); + if (!allowAll && !matched) + return; + + if (allowAll) + response->addHeader("Access-Control-Allow-Origin", "*"); + else + response->addHeader("Access-Control-Allow-Origin", origin); + response->addHeader("Access-Control-Allow-Credentials", "true"); response->addHeader("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS"); response->addHeader("Access-Control-Allow-Headers", "Authorization,Content-Type"); + response->addHeader("Vary", "Origin"); } } // namespace diff --git a/config.json b/config.json index 66782bc..e7bfd4d 100644 --- a/config.json +++ b/config.json @@ -63,7 +63,7 @@ "spec_path": "build/openapi/visione.openapi.json" }, "cors": { - "allowed_origin": "http://localhost:5173" + "allowed_origin": ["http://localhost:3000", "http://localhost:5137"] } } -} +} \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..3ad1189 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,63 @@ +services: + backend: + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + volumes: + - uploads-data:/app/uploads + environment: + DB_HOST: ${DB_HOST:-mysql} + DB_PORT: ${DB_PORT:-3306} + DB_USER: ${MYSQL_USER:-visione} + DB_PASSWORD: ${MYSQL_PASSWORD:-visione} + DB_NAME: ${MYSQL_DATABASE:-visione} + SMTP_HOST: ${SMTP_HOST:-mailhog} + SMTP_PORT: ${SMTP_PORT:-1025} + expose: + - "5555" + networks: + - visione + - dokploy-network + depends_on: + mysql: + condition: service_healthy + + mysql: + image: mysql:8.4 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:-root}" + MYSQL_DATABASE: "${MYSQL_DATABASE:-visione}" + MYSQL_USER: "${MYSQL_USER:-visione}" + MYSQL_PASSWORD: "${MYSQL_PASSWORD:-visione}" + expose: + - "3306" + volumes: + - mysql-data:/var/lib/mysql + networks: + - visione + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u$${MYSQL_USER} -p$${MYSQL_PASSWORD} --silent"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 20s + + mailhog: + image: axllent/mailpit:latest + restart: unless-stopped + expose: + - "1025" + - "8025" + networks: + - visione + +volumes: + mysql-data: + uploads-data: + +networks: + visione: + dokploy-network: + external: true diff --git a/main.cc b/main.cc index 933dc4c..7854cc0 100644 --- a/main.cc +++ b/main.cc @@ -5,16 +5,46 @@ #include "repositories/MigrationRunner.h" #include +#include +#include int main() { setvbuf(stdout, nullptr, _IONBF, 0); (void)RequestLogger::classTypeName(); + Json::Value config; + std::ifstream configFile("config.json"); + if (configFile.is_open()) + { + configFile >> config; + + if (const char* envHost = std::getenv("DB_HOST")) + config["db_clients"][0]["host"] = envHost; + if (const char* envPort = std::getenv("DB_PORT")) + config["db_clients"][0]["port"] = std::stoi(envPort); + if (const char* envUser = std::getenv("DB_USER")) + config["db_clients"][0]["user"] = envUser; + if (const char* envPass = std::getenv("DB_PASSWORD")) + config["db_clients"][0]["passwd"] = envPass; + if (const char* envDb = std::getenv("DB_NAME")) + config["db_clients"][0]["dbname"] = envDb; + + if (const char* envSmtpHost = std::getenv("SMTP_HOST")) + config["custom_config"]["mail"]["smtp_host"] = envSmtpHost; + if (const char* envSmtpPort = std::getenv("SMTP_PORT")) + config["custom_config"]["mail"]["smtp_port"] = std::stoi(envSmtpPort); + + drogon::app().loadConfigJson(config); + } + else + { + drogon::app().loadConfigFile("config.json"); + } + Json::Value requestLoggerConfig; requestLoggerConfig["include_query"] = true; - drogon::app().loadConfigFile("config.json"); drogon::app().addPlugin("RequestLogger", {}, requestLoggerConfig); visione::cors::configure();