Remove API key requirement
This commit is contained in:
@@ -4,7 +4,6 @@ on:
|
||||
# Manual only for now. Select the branch to build when dispatching:
|
||||
# main -> APP_VERSION is the current git tag
|
||||
# dev -> APP_VERSION is the current commit hash
|
||||
# Both branches bake the portal API key from secrets.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -44,28 +43,18 @@ jobs:
|
||||
else
|
||||
APP_VERSION="$(git rev-parse --short HEAD)"
|
||||
fi
|
||||
echo "APP_VERSION=$APP_VERSION" >> "$GITHUB_ENV"
|
||||
echo "Branch: ${{ gitea.ref_name }} -> APP_VERSION=$APP_VERSION"
|
||||
if [ -n "${{ secrets.RWT_API_KEY }}" ]; then
|
||||
echo "API key: provided via secret RWT_API_KEY"
|
||||
else
|
||||
echo "API key: NOT set (build proceeds without an Authorization header)"
|
||||
fi
|
||||
|
||||
# checks is keyed per-system (checks.<system>.default); nix build's
|
||||
# #attr shorthand can't resolve it, so resolve the current system inline.
|
||||
# --impure lets the flake read APP_VERSION/API_KEY via builtins.getEnv
|
||||
# --impure lets the flake read APP_VERSION via builtins.getEnv
|
||||
# (pure evals always get the package defaults). The offline test suite
|
||||
# runs in the package's checkPhase.
|
||||
- name: Build + offline test suite
|
||||
env:
|
||||
API_KEY: ${{ secrets.RWT_API_KEY }}
|
||||
run: nix build -L ".#checks.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" --impure
|
||||
|
||||
- name: Live portal tests
|
||||
if: ${{ secrets.RWT_LIVE_API_KEY != '' }}
|
||||
env:
|
||||
RWT_LIVE_API_KEY: ${{ secrets.RWT_LIVE_API_KEY }}
|
||||
RWT_LIVE_ACCOUNT_EMAIL: ${{ secrets.RWT_LIVE_ACCOUNT_EMAIL }}
|
||||
RWT_LIVE_LICENSE_EMAIL: ${{ secrets.RWT_LIVE_LICENSE_EMAIL }}
|
||||
RWT_LIVE_LICENSE_KEY: ${{ secrets.RWT_LIVE_LICENSE_KEY }}
|
||||
|
||||
@@ -33,23 +33,8 @@ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DAPP_VERSION="1.2.3"
|
||||
The Nix package bakes `-DAPP_VERSION=<pkg version>` via `cmakeFlags` (`version`
|
||||
defaults to `0.1.0` in `nix/package.nix`; the flake overrides it from the
|
||||
`APP_VERSION` env var on impure evals). `nix develop`
|
||||
sets `APP_VERSION=dev` and exports `API_KEY` from the ambient environment
|
||||
(falling back to `RWT_LIVE_API_KEY`). CMake seeds a *fresh* configure from the
|
||||
`APP_VERSION` / `API_KEY` env vars; `-D` wins on reconfigure.
|
||||
|
||||
The portal API key is also a build-time argument: `-DAPI_KEY="..."` (CMake) or
|
||||
`apiKey` on `callPackage` (Nix). It is compiled into the binary as the
|
||||
`Authorization: Bearer` token and defaults to empty (no auth header). Keep the
|
||||
real value in CI secrets, never in source — rotate by rebuilding with a new
|
||||
key.
|
||||
|
||||
Version and API key can be baked in together with a single configure:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug \
|
||||
-DAPP_VERSION="1.2.3" \
|
||||
-DAPI_KEY="$API_KEY"
|
||||
```
|
||||
sets `APP_VERSION=dev`. CMake seeds a *fresh* configure from the
|
||||
`APP_VERSION` env var; `-D` wins on reconfigure.
|
||||
|
||||
- The app resolves `assets/...` relative to its working directory — launch
|
||||
`./build/radiantwave` from the repo root. (The Nix wrapper chdirs to
|
||||
@@ -238,14 +223,11 @@ cached.
|
||||
`checks` is keyed per-system, so the workflow resolves the current system
|
||||
via `nix eval --impure --expr 'builtins.currentSystem'`). The build bakes
|
||||
in a per-branch `APP_VERSION` — the current git tag on `main`, the current
|
||||
commit hash on `dev` — plus the portal API key from the `RWT_API_KEY`
|
||||
secret. The flake reads `APP_VERSION`/`API_KEY` from the environment via
|
||||
commit hash on `dev`. The flake reads `APP_VERSION` from the environment via
|
||||
`builtins.getEnv`, so the build must pass `--impure` (pure evals get the
|
||||
package defaults). When the `RWT_LIVE_API_KEY` secret is set it also runs
|
||||
`test_api_client` / `test_license_service` against the portal via
|
||||
`nix develop` (outside the sandbox, like a dev machine). Those live tests
|
||||
also need `RWT_LIVE_ACCOUNT_EMAIL`, `RWT_LIVE_LICENSE_EMAIL`, and
|
||||
`RWT_LIVE_LICENSE_KEY`.
|
||||
package defaults). The live portal tests run from `nix develop` when the
|
||||
`RWT_LIVE_*` secrets are set and need `RWT_LIVE_ACCOUNT_EMAIL`,
|
||||
`RWT_LIVE_LICENSE_EMAIL`, and `RWT_LIVE_LICENSE_KEY`.
|
||||
- `test_assets` must pass before committing — it catches missing fonts/images
|
||||
that break the app at runtime.
|
||||
- `cmake --build build` builds everything; pass `--target <name>` to build one
|
||||
|
||||
@@ -18,20 +18,6 @@ else()
|
||||
"Version string shown in the Home footer (override at configure time)")
|
||||
endif()
|
||||
|
||||
# API bearer key baked into the binary and sent as `Authorization: Bearer <key>`
|
||||
# on every portal request. Leave empty to send no auth header. Never commit the
|
||||
# real value — inject it at CI build time from a secret, e.g.:
|
||||
# cmake ... -DAPI_KEY="${{ secrets.PORTAL_API_KEY }}"
|
||||
# The `nix develop` shell exports API_KEY from the ambient environment, which
|
||||
# seeds a fresh configure; -D wins on reconfigure.
|
||||
if(DEFINED ENV{API_KEY})
|
||||
set(API_KEY "$ENV{API_KEY}" CACHE STRING
|
||||
"Portal API key baked into the binary (leave empty to skip auth)")
|
||||
else()
|
||||
set(API_KEY "" CACHE STRING
|
||||
"Portal API key baked into the binary (leave empty to skip auth)")
|
||||
endif()
|
||||
|
||||
find_package(SDL3 REQUIRED)
|
||||
find_package(SDL3_ttf REQUIRED)
|
||||
find_package(SDL3_image REQUIRED)
|
||||
@@ -90,7 +76,6 @@ add_executable(radiantwave
|
||||
|
||||
target_compile_definitions(radiantwave PRIVATE
|
||||
APP_VERSION="${APP_VERSION}"
|
||||
API_KEY="${API_KEY}"
|
||||
)
|
||||
|
||||
target_include_directories(radiantwave PRIVATE src)
|
||||
|
||||
@@ -136,45 +136,13 @@ If `APP_VERSION` is left unset the footer falls back to the project version
|
||||
from `CMakeLists.txt` (`project(... VERSION ...)`); a build that defines no
|
||||
version at all shows `unknown`. The Nix build sets `APP_VERSION` automatically
|
||||
to the flake package version, so a plain `nix build` needs no extra flags.
|
||||
With `--impure` the flake instead reads the `APP_VERSION` / `API_KEY`
|
||||
environment variables — this is how CI injects the version and portal key
|
||||
With `--impure` the flake instead reads the `APP_VERSION`
|
||||
environment variable — this is how CI injects the version
|
||||
(see the CI note under *Common Pitfalls*). Pure evals always get the package
|
||||
defaults, because `builtins.getEnv` cannot see the environment in pure
|
||||
evaluation mode.
|
||||
|
||||
#### Portal API key
|
||||
|
||||
Every portal request sends `Authorization: Bearer <key>`. The key is baked
|
||||
into the binary at build time via the `API_KEY` CMake cache variable. It is
|
||||
**optional** — when unset, requests go out without an auth header. Keep the
|
||||
real value out of the source tree and inject it at build time from your CI
|
||||
secret store so it never lands in `git`:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DAPI_KEY="${{ secrets.PORTAL_API_KEY }}"
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
Rotate the key by building with a new value and shipping the new binary. The
|
||||
Nix package accepts the same value via `callPackage ./nix/package.nix {
|
||||
apiKey = "…"; }`, or via the `API_KEY` environment variable with
|
||||
`nix build --impure`; the flake itself never embeds one.
|
||||
|
||||
#### Version + API key in one configure
|
||||
|
||||
`APP_VERSION` and `API_KEY` are independent CMake cache variables, so pass
|
||||
both — plus the build type — in a single configure:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug \
|
||||
-DAPP_VERSION="1.2.3" \
|
||||
-DAPI_KEY="${{ secrets.PORTAL_API_KEY }}"
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
`-D` values always win over the `APP_VERSION` / `API_KEY` environment
|
||||
variables, which `nix develop` seeds from the ambient environment.
|
||||
#### Version in one configure
|
||||
|
||||
### Building with Nix
|
||||
|
||||
@@ -187,14 +155,14 @@ nix build .#packages.x86_64-linux.default # needs a Linux machine/builders
|
||||
nix build .#packages.aarch64-linux.default
|
||||
```
|
||||
|
||||
To bake in a custom version and/or portal key, build impurely — the flake
|
||||
reads `APP_VERSION` and `API_KEY` from the environment on impure evals:
|
||||
To bake in a custom version, build impurely — the flake
|
||||
reads `APP_VERSION` from the environment on impure evals:
|
||||
|
||||
```sh
|
||||
APP_VERSION="1.2.3" API_KEY="$PORTAL_API_KEY" nix build --impure .#default
|
||||
APP_VERSION="1.2.3" nix build --impure .#default
|
||||
```
|
||||
|
||||
A pure `nix build` always uses the package defaults (`0.1.0`, no auth header)
|
||||
A pure `nix build` always uses the package defaults (`0.1.0`)
|
||||
even if those variables are set in your shell, because pure evaluation cannot
|
||||
see the environment. CI uses `--impure` to inject them.
|
||||
|
||||
@@ -633,9 +601,9 @@ status to decide which page to show.
|
||||
workflow is `.gitea/workflows/build.yml`. It is **manual-only**
|
||||
(`workflow_dispatch`) — pick the branch when dispatching. `main` builds bake
|
||||
the current git tag as `APP_VERSION`; `dev` builds bake the current commit
|
||||
hash. Both pass the portal key from the `RWT_API_KEY` secret and build with
|
||||
`nix build --impure` (which lets the flake's env injection work). The
|
||||
offline test suite runs in the package's `checkPhase`; the live portal tests
|
||||
run from `nix develop` when the `RWT_LIVE_*` secrets are set.
|
||||
hash. Both build with `nix build --impure` (which lets the flake's env
|
||||
injection work). The offline test suite runs in the package's `checkPhase`;
|
||||
the live portal tests run from `nix develop` when the `RWT_LIVE_*` secrets
|
||||
are set.
|
||||
- Asset tests (`test_assets`) must pass before committing -- they catch missing
|
||||
fonts and images that would break the app at runtime.
|
||||
|
||||
@@ -34,18 +34,16 @@
|
||||
};
|
||||
in
|
||||
{
|
||||
# Version and API key are injected from the ambient environment on
|
||||
# impure evals (CI sets APP_VERSION/API_KEY from the git ref and a
|
||||
# secret). Pure evals (`nix flake check`, local `nix build`) can't
|
||||
# see the environment, so builtins.getEnv returns "" and the
|
||||
# package falls back to its 0.1.0 default / no auth header.
|
||||
# Version is injected from the ambient environment on
|
||||
# impure evals (CI sets APP_VERSION from the git ref). Pure evals
|
||||
# (`nix flake check`, local `nix build`) can't see the
|
||||
# environment, so builtins.getEnv returns "" and the package
|
||||
# falls back to its 0.1.0 default.
|
||||
packages.default =
|
||||
let
|
||||
envVersion = builtins.getEnv "APP_VERSION";
|
||||
envApiKey = builtins.getEnv "API_KEY";
|
||||
packageArgs =
|
||||
{ apiKey = envApiKey; }
|
||||
// (nixpkgs.lib.optionalAttrs (envVersion != "") { version = envVersion; });
|
||||
nixpkgs.lib.optionalAttrs (envVersion != "") { version = envVersion; };
|
||||
in
|
||||
pkgs.callPackage ./nix/package.nix packageArgs;
|
||||
|
||||
@@ -53,22 +51,17 @@
|
||||
inputsFrom = [ self.packages.${system}.default ];
|
||||
packages = [ ];
|
||||
env = {
|
||||
# APP_VERSION and API_KEY are CMake cache variables with no
|
||||
# default; on a fresh configure cmake seeds their cache from
|
||||
# environment variables of the same name, so a plain
|
||||
# `cmake -S . -B build` bakes them into the binary.
|
||||
# APP_VERSION is a CMake cache variable with no default; on a
|
||||
# fresh configure cmake seeds its cache from the environment
|
||||
# variable of the same name, so a plain `cmake -S . -B build`
|
||||
# bakes it into the binary.
|
||||
APP_VERSION = "dev";
|
||||
};
|
||||
shellHook = ''
|
||||
# Never commit the real key. Export API_KEY (or the CI name
|
||||
# RWT_LIVE_API_KEY) in your ambient environment and this shell
|
||||
# picks it up; empty by default (no auth header).
|
||||
export API_KEY="''${API_KEY:-''${RWT_LIVE_API_KEY:-}}"
|
||||
|
||||
echo "RadiantWave dev shell (APP_VERSION=dev, API_KEY=$([[ -n "$API_KEY" ]] && echo set || echo unset))"
|
||||
echo "RadiantWave dev shell (APP_VERSION=dev)"
|
||||
echo " Configure: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug"
|
||||
echo " Bake version+key: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DAPP_VERSION=\"1.2.3\" -DAPI_KEY=\"<key>\""
|
||||
echo " Nix build (env version+key, impure): APP_VERSION=... API_KEY=... nix build --impure .#default"
|
||||
echo " Bake version: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DAPP_VERSION=\"1.2.3\""
|
||||
echo " Nix build (env version, impure): APP_VERSION=... nix build --impure .#default"
|
||||
echo " Build: cmake --build build"
|
||||
echo " Run: ./build/radiantwave"
|
||||
echo " Tests: ./build/test_assets && ./build/test_<name>"
|
||||
|
||||
+1
-7
@@ -15,9 +15,6 @@
|
||||
curl,
|
||||
openssl,
|
||||
systemdLibs,
|
||||
# Portal API key baked into the binary. Keep it out of the flake; pass it
|
||||
# when calling callPackage from CI/your own config (see README "App version").
|
||||
apiKey ? "",
|
||||
# App version baked into the Home footer's Version row. CI overrides this
|
||||
# per branch (git tag on main, commit hash on dev); local/pure builds keep
|
||||
# the 0.1.0 default.
|
||||
@@ -54,10 +51,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
|
||||
# Bake the package version into the binary so the Home footer's Version row
|
||||
# shows it (CMake passes APP_VERSION through as a compile definition).
|
||||
# API_KEY is only added when a non-empty key is supplied to callPackage.
|
||||
cmakeFlags =
|
||||
[ "-DAPP_VERSION=${finalAttrs.version}" ]
|
||||
++ lib.optionals (apiKey != "") [ "-DAPI_KEY=${apiKey}" ];
|
||||
cmakeFlags = [ "-DAPP_VERSION=${finalAttrs.version}" ];
|
||||
|
||||
# gtest is a buildInput, not native: nixpkgs builds it shared, so the
|
||||
# check-phase test binaries need its lib dir baked into their RPATH.
|
||||
|
||||
@@ -97,7 +97,7 @@ void LicenseKeyPage::submit()
|
||||
// When the email is missing the key is saved but the check is skipped.
|
||||
std::string errorMessage;
|
||||
const radiantwave::license::Result result = radiantwave::license::validateStoredLicense(
|
||||
state_->db, "", &errorMessage);
|
||||
state_->db, &errorMessage);
|
||||
|
||||
switch (result) {
|
||||
case radiantwave::license::Result::Ok:
|
||||
|
||||
+13
-26
@@ -4,10 +4,6 @@
|
||||
#include <curl/curl.h>
|
||||
#include <string>
|
||||
|
||||
#ifndef API_KEY
|
||||
#define API_KEY ""
|
||||
#endif
|
||||
|
||||
namespace radiantwave::api {
|
||||
|
||||
namespace {
|
||||
@@ -31,7 +27,7 @@ size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
||||
}
|
||||
|
||||
// Shared curl setup + perform. Returns a populated HttpResponse.
|
||||
HttpResponse performRequest(const std::string& url, const std::string& apiKey,
|
||||
HttpResponse performRequest(const std::string& url,
|
||||
bool post, const std::string& jsonBody) {
|
||||
HttpResponse response;
|
||||
|
||||
@@ -47,12 +43,6 @@ HttpResponse performRequest(const std::string& url, const std::string& apiKey,
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body);
|
||||
|
||||
struct curl_slist* headers = nullptr;
|
||||
// An explicit apiKey wins; otherwise fall back to the key baked in at
|
||||
// build time via -DAPI_KEY (empty string when unset).
|
||||
const std::string effectiveKey = apiKey.empty() ? API_KEY : apiKey;
|
||||
if (!effectiveKey.empty()) {
|
||||
headers = curl_slist_append(headers, ("Authorization: Bearer " + effectiveKey).c_str());
|
||||
}
|
||||
if (post) {
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
@@ -79,13 +69,12 @@ HttpResponse performRequest(const std::string& url, const std::string& apiKey,
|
||||
|
||||
// ─── HTTP helpers ──────────────────────────────────────────────────────
|
||||
|
||||
HttpResponse httpGet(const std::string& path, const std::string& apiKey) {
|
||||
return performRequest(urlFor(path), apiKey, false, "");
|
||||
HttpResponse httpGet(const std::string& path) {
|
||||
return performRequest(urlFor(path), false, "");
|
||||
}
|
||||
|
||||
HttpResponse httpPost(const std::string& path, const std::string& jsonBody,
|
||||
const std::string& apiKey) {
|
||||
return performRequest(urlFor(path), apiKey, true, jsonBody);
|
||||
HttpResponse httpPost(const std::string& path, const std::string& jsonBody) {
|
||||
return performRequest(urlFor(path), true, jsonBody);
|
||||
}
|
||||
|
||||
// ─── GET /frequencies ──────────────────────────────────────────────────
|
||||
@@ -112,8 +101,8 @@ bool parseFrequenciesJson(const std::string& body, std::vector<Frequency>& out)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fetchFrequencies(std::vector<Frequency>& out, const std::string& apiKey) {
|
||||
HttpResponse response = httpGet(PATH_FREQUENCIES, apiKey);
|
||||
bool fetchFrequencies(std::vector<Frequency>& out) {
|
||||
HttpResponse response = httpGet(PATH_FREQUENCIES);
|
||||
if (!response.transportOk || response.status < 200 || response.status >= 300) {
|
||||
return false;
|
||||
}
|
||||
@@ -169,8 +158,8 @@ bool parseColorPalettesJson(const std::string& body, std::vector<ColorPalette>&
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fetchColorPalettes(std::vector<ColorPalette>& out, const std::string& apiKey) {
|
||||
HttpResponse response = httpGet(PATH_COLOR_PALETTES, apiKey);
|
||||
bool fetchColorPalettes(std::vector<ColorPalette>& out) {
|
||||
HttpResponse response = httpGet(PATH_COLOR_PALETTES);
|
||||
if (!response.transportOk || response.status < 200 || response.status >= 300) {
|
||||
return false;
|
||||
}
|
||||
@@ -196,13 +185,12 @@ bool parseLicenseJson(const std::string& body, LicenseInfo& out) {
|
||||
return !out.key.empty();
|
||||
}
|
||||
|
||||
LicenseResult validateLicense(const std::string& key, const std::string& email,
|
||||
const std::string& apiKey) {
|
||||
LicenseResult validateLicense(const std::string& key, const std::string& email) {
|
||||
LicenseResult result;
|
||||
|
||||
const std::string jsonBody =
|
||||
"{\"license_key\":\"" + key + "\",\"email_address\":\"" + email + "\"}";
|
||||
HttpResponse response = httpPost(PATH_LICENSE, jsonBody, apiKey);
|
||||
HttpResponse response = httpPost(PATH_LICENSE, jsonBody);
|
||||
result.status = response.status;
|
||||
|
||||
if (!response.transportOk) {
|
||||
@@ -233,8 +221,7 @@ bool parseCreateAccountJson(const std::string& body, AccountBody& out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
AccountResult createAccount(const std::string& email, const std::string& name,
|
||||
const std::string& apiKey) {
|
||||
AccountResult createAccount(const std::string& email, const std::string& name) {
|
||||
AccountResult result;
|
||||
|
||||
std::string jsonBody =
|
||||
@@ -244,7 +231,7 @@ AccountResult createAccount(const std::string& email, const std::string& name,
|
||||
}
|
||||
jsonBody += "}";
|
||||
|
||||
HttpResponse response = httpPost(PATH_CREATE_ACCOUNT, jsonBody, apiKey);
|
||||
HttpResponse response = httpPost(PATH_CREATE_ACCOUNT, jsonBody);
|
||||
result.status = response.status;
|
||||
|
||||
if (!response.transportOk) {
|
||||
|
||||
@@ -24,14 +24,9 @@ struct HttpResponse {
|
||||
bool transportOk = false; // true when the request completed without a transport error
|
||||
};
|
||||
|
||||
// Performs a GET/POST against `path` (relative to API_BASE_URL). When `apiKey` is
|
||||
// non-empty it is sent as `Authorization: Bearer <apiKey>`. When it is empty (or
|
||||
// omitted) the request falls back to the key baked in at build time via the
|
||||
// `API_KEY` compile definition (see CMakeLists.txt); with neither set, no auth
|
||||
// header is sent.
|
||||
HttpResponse httpGet(const std::string& path, const std::string& apiKey = "");
|
||||
HttpResponse httpPost(const std::string& path, const std::string& jsonBody,
|
||||
const std::string& apiKey = "");
|
||||
// Performs a GET/POST against `path` (relative to API_BASE_URL).
|
||||
HttpResponse httpGet(const std::string& path);
|
||||
HttpResponse httpPost(const std::string& path, const std::string& jsonBody);
|
||||
|
||||
// ─── GET /frequencies ──────────────────────────────────────────────────
|
||||
|
||||
@@ -45,7 +40,7 @@ struct Frequency {
|
||||
bool parseFrequenciesJson(const std::string& body, std::vector<Frequency>& out);
|
||||
|
||||
// Fetch the solfeggio frequencies. Returns true only on HTTP 2xx + successful parse.
|
||||
bool fetchFrequencies(std::vector<Frequency>& out, const std::string& apiKey = "");
|
||||
bool fetchFrequencies(std::vector<Frequency>& out);
|
||||
|
||||
// ─── GET /color-palettes ───────────────────────────────────────────────
|
||||
|
||||
@@ -75,7 +70,7 @@ bool parseColorPalettesJson(const std::string& body, std::vector<ColorPalette>&
|
||||
// NOTE: the portal does not expose this route yet. This is readiness code — it
|
||||
// is deliberately not called anywhere in the app until the route ships, so
|
||||
// palettes can be updated from the portal rather than code releases.
|
||||
bool fetchColorPalettes(std::vector<ColorPalette>& out, const std::string& apiKey = "");
|
||||
bool fetchColorPalettes(std::vector<ColorPalette>& out);
|
||||
|
||||
// ─── POST /license ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -98,8 +93,7 @@ struct LicenseResult {
|
||||
// Validate a license key / email pair against the portal. Never throws; failures
|
||||
// (network, HTTP error status) are reported through the returned LicenseResult.
|
||||
// A 2xx response means the license is valid and the subscription is active.
|
||||
LicenseResult validateLicense(const std::string& key, const std::string& email,
|
||||
const std::string& apiKey = "");
|
||||
LicenseResult validateLicense(const std::string& key, const std::string& email);
|
||||
|
||||
// ─── POST /automations/create/account ──────────────────────────────────
|
||||
|
||||
@@ -122,7 +116,6 @@ struct AccountResult {
|
||||
|
||||
// Creates (or reuses) a user account and issues a license key. The key is emailed
|
||||
// to the user and is NOT part of the response. `name` may be empty.
|
||||
AccountResult createAccount(const std::string& email, const std::string& name = "",
|
||||
const std::string& apiKey = "");
|
||||
AccountResult createAccount(const std::string& email, const std::string& name = "");
|
||||
|
||||
} // namespace radiantwave::api
|
||||
|
||||
@@ -17,7 +17,6 @@ long long nowEpochSeconds() {
|
||||
} // namespace
|
||||
|
||||
Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
|
||||
const std::string& apiKey,
|
||||
std::string* outErrorMessage) {
|
||||
if (outErrorMessage) outErrorMessage->clear();
|
||||
|
||||
@@ -28,7 +27,7 @@ Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
|
||||
}
|
||||
|
||||
const radiantwave::api::LicenseResult result =
|
||||
radiantwave::api::validateLicense(key, email, apiKey);
|
||||
radiantwave::api::validateLicense(key, email);
|
||||
|
||||
if (result.status == 0) {
|
||||
if (outErrorMessage) *outErrorMessage = "network error";
|
||||
|
||||
@@ -26,7 +26,6 @@ constexpr long long GRACE_PERIOD_SECONDS = 7LL * 24 * 60 * 60;
|
||||
//
|
||||
// `outErrorMessage` is an optional out-param.
|
||||
Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
|
||||
const std::string& apiKey = "",
|
||||
std::string* outErrorMessage = nullptr);
|
||||
|
||||
// Records the current time as the "last verified" stamp of the license. Called
|
||||
|
||||
@@ -190,10 +190,9 @@ bool parseProclamationPalettesJson(const std::string& body,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db,
|
||||
const std::string& apiKey) {
|
||||
bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db) {
|
||||
std::vector<api::ColorPalette> palettes;
|
||||
if (!api::fetchColorPalettes(palettes, apiKey)) {
|
||||
if (!api::fetchColorPalettes(palettes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -211,9 +210,8 @@ bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db,
|
||||
return db.replaceColorPalettes(records);
|
||||
}
|
||||
|
||||
bool syncProclamationPalettesFromApi(radiantwave::db::DatabaseManager& db,
|
||||
const std::string& apiKey) {
|
||||
api::HttpResponse response = api::httpGet(api::PATH_PROCLAMATIONS, apiKey);
|
||||
bool syncProclamationPalettesFromApi(radiantwave::db::DatabaseManager& db) {
|
||||
api::HttpResponse response = api::httpGet(api::PATH_PROCLAMATIONS);
|
||||
if (!response.transportOk || response.status < 200 || response.status >= 300) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -37,13 +37,11 @@ bool parseProclamationPalettesJson(const std::string& body,
|
||||
// NOTE: not yet wired into the app flow — the portal does not expose the route
|
||||
// yet. Readiness code: palettes should be updated from the portal, not code
|
||||
// releases, so callers should treat this as a future sync step.
|
||||
bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db,
|
||||
const std::string& apiKey = "");
|
||||
bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db);
|
||||
|
||||
// Alternative to syncColorPalettesFromApi: fetch proclamations and replace the
|
||||
// local palette table using the palettes embedded in each proclamation.
|
||||
// NOTE: not yet wired into the app flow; see syncColorPalettesFromApi.
|
||||
bool syncProclamationPalettesFromApi(radiantwave::db::DatabaseManager& db,
|
||||
const std::string& apiKey = "");
|
||||
bool syncProclamationPalettesFromApi(radiantwave::db::DatabaseManager& db);
|
||||
|
||||
} // namespace radiantwave::sync
|
||||
|
||||
@@ -19,11 +19,6 @@ std::string env(const char* name) {
|
||||
return value ? std::string(value) : std::string();
|
||||
}
|
||||
|
||||
// Returns the optional live API key, or "" when none was provided.
|
||||
std::string liveApiKey() {
|
||||
return env("RW_LIVE_API_KEY");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ─── Offline: parse functions ─────────────────────────────────────────
|
||||
@@ -163,7 +158,7 @@ TEST(ApiClientTest, ParseCategoriesHandlesNullParentId) {
|
||||
|
||||
TEST(ApiClientLiveTest, FetchFrequenciesLive) {
|
||||
std::vector<Frequency> out;
|
||||
EXPECT_TRUE(fetchFrequencies(out, liveApiKey()));
|
||||
EXPECT_TRUE(fetchFrequencies(out));
|
||||
ASSERT_GE(out.size(), 7u);
|
||||
|
||||
bool foundSolarPlexus = false;
|
||||
@@ -180,7 +175,7 @@ TEST(ApiClientLiveTest, FetchFrequenciesLive) {
|
||||
// ─── Live: proclamation + category parsers against the real API ────────
|
||||
|
||||
TEST(ApiClientLiveTest, ProclamationsAndCategoriesLiveSmoke) {
|
||||
HttpResponse procsResponse = httpGet(PATH_PROCLAMATIONS, liveApiKey());
|
||||
HttpResponse procsResponse = httpGet(PATH_PROCLAMATIONS);
|
||||
ASSERT_TRUE(procsResponse.transportOk);
|
||||
ASSERT_EQ(procsResponse.status, 200);
|
||||
|
||||
@@ -193,7 +188,7 @@ TEST(ApiClientLiveTest, ProclamationsAndCategoriesLiveSmoke) {
|
||||
EXPECT_FALSE(proclamations[0].id.empty());
|
||||
}
|
||||
|
||||
HttpResponse catsResponse = httpGet(PATH_CATEGORIES, liveApiKey());
|
||||
HttpResponse catsResponse = httpGet(PATH_CATEGORIES);
|
||||
ASSERT_TRUE(catsResponse.transportOk);
|
||||
ASSERT_EQ(catsResponse.status, 200);
|
||||
|
||||
@@ -205,14 +200,14 @@ TEST(ApiClientLiveTest, ProclamationsAndCategoriesLiveSmoke) {
|
||||
// ─── Live: POST /license error paths (no credentials required) ─────────
|
||||
|
||||
TEST(ApiClientLiveTest, ValidateLicenseMissingKeyLive) {
|
||||
LicenseResult result = validateLicense("", "test@example.com", liveApiKey());
|
||||
LicenseResult result = validateLicense("", "test@example.com");
|
||||
ASSERT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status, 400);
|
||||
EXPECT_EQ(result.error_message, "license_key is required");
|
||||
}
|
||||
|
||||
TEST(ApiClientLiveTest, ValidateLicenseMissingEmailLive) {
|
||||
LicenseResult result = validateLicense("AAAA-AAAA-AAAA-AAAA", "", liveApiKey());
|
||||
LicenseResult result = validateLicense("AAAA-AAAA-AAAA-AAAA", "");
|
||||
ASSERT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status, 400);
|
||||
EXPECT_EQ(result.error_message, "email_address is required");
|
||||
@@ -220,7 +215,7 @@ TEST(ApiClientLiveTest, ValidateLicenseMissingEmailLive) {
|
||||
|
||||
TEST(ApiClientLiveTest, ValidateLicenseUnknownKeyLive) {
|
||||
LicenseResult result =
|
||||
validateLicense("AAAA-AAAA-AAAA-AAAA", "nobody@example.com", liveApiKey());
|
||||
validateLicense("AAAA-AAAA-AAAA-AAAA", "nobody@example.com");
|
||||
ASSERT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status, 400);
|
||||
EXPECT_EQ(result.error_message, "license key not found");
|
||||
@@ -235,7 +230,7 @@ TEST(ApiClientLiveTest, ValidateLicenseSuccessLive) {
|
||||
GTEST_SKIP() << "RW_LIVE_LICENSE_KEY / RW_LIVE_LICENSE_EMAIL not set";
|
||||
}
|
||||
|
||||
LicenseResult result = validateLicense(key, email, liveApiKey());
|
||||
LicenseResult result = validateLicense(key, email);
|
||||
ASSERT_TRUE(result.ok()) << "status=" << result.status
|
||||
<< " error=" << result.error_message;
|
||||
EXPECT_EQ(result.info.key, key);
|
||||
@@ -245,7 +240,7 @@ TEST(ApiClientLiveTest, ValidateLicenseSuccessLive) {
|
||||
// ─── Live: POST /automations/create/account error paths (harmless) ─────
|
||||
|
||||
TEST(ApiClientLiveTest, CreateAccountMissingEmailLive) {
|
||||
AccountResult result = createAccount("", "", liveApiKey());
|
||||
AccountResult result = createAccount("", "");
|
||||
ASSERT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status, 400);
|
||||
EXPECT_EQ(result.error_message, "email is required");
|
||||
@@ -260,7 +255,7 @@ TEST(ApiClientLiveTest, CreateAccountSuccessLive) {
|
||||
GTEST_SKIP() << "RW_LIVE_ACCOUNT_EMAIL not set";
|
||||
}
|
||||
|
||||
AccountResult result = createAccount(email, "RadiantWave Test", liveApiKey());
|
||||
AccountResult result = createAccount(email, "RadiantWave Test");
|
||||
ASSERT_TRUE(result.ok()) << "status=" << result.status
|
||||
<< " error=" << result.error_message;
|
||||
EXPECT_TRUE(result.body.success);
|
||||
|
||||
@@ -175,7 +175,7 @@ TEST(ColorPalettesLiveTest, FetchColorPalettesLive) {
|
||||
}
|
||||
|
||||
std::vector<ColorPalette> palettes;
|
||||
ASSERT_TRUE(fetchColorPalettes(palettes, env("RW_LIVE_API_KEY")));
|
||||
ASSERT_TRUE(fetchColorPalettes(palettes));
|
||||
EXPECT_FALSE(palettes.empty());
|
||||
for (const auto& p : palettes) {
|
||||
EXPECT_GE(p.colors.size(), 1u);
|
||||
|
||||
@@ -48,7 +48,7 @@ TEST(LicenseServiceTest, NotConfiguredWhenNoEmail) {
|
||||
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
|
||||
|
||||
std::string errorMessage = "stale";
|
||||
EXPECT_EQ(validateStoredLicense(tmp.db, "", &errorMessage),
|
||||
EXPECT_EQ(validateStoredLicense(tmp.db, &errorMessage),
|
||||
Result::NotConfigured);
|
||||
EXPECT_EQ(errorMessage, "");
|
||||
}
|
||||
@@ -57,7 +57,7 @@ TEST(LicenseServiceTest, NotConfiguredWhenNoLicense) {
|
||||
TempDb tmp;
|
||||
tmp.db.setKV(KV_EMAIL_ADDRESS, "user@example.com");
|
||||
|
||||
EXPECT_EQ(validateStoredLicense(tmp.db, "", nullptr),
|
||||
EXPECT_EQ(validateStoredLicense(tmp.db, nullptr),
|
||||
Result::NotConfigured);
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ TEST(LicenseServiceLiveTest, ValidatesAndGrantsSubscribedState) {
|
||||
tmp.db.setKV(KV_EMAIL_ADDRESS, email);
|
||||
|
||||
std::string errorMessage;
|
||||
const Result result = validateStoredLicense(tmp.db, "", &errorMessage);
|
||||
const Result result = validateStoredLicense(tmp.db, &errorMessage);
|
||||
EXPECT_EQ(result, Result::Ok) << "error=" << errorMessage;
|
||||
|
||||
// A successful validation is the "subscribed" flag; no subscription-level
|
||||
@@ -134,7 +134,7 @@ TEST(LicenseServiceLiveTest, InvalidPairReportsInvalid) {
|
||||
tmp.db.setKV(KV_EMAIL_ADDRESS, "nobody@example.com");
|
||||
|
||||
std::string errorMessage;
|
||||
EXPECT_EQ(validateStoredLicense(tmp.db, "", &errorMessage),
|
||||
EXPECT_EQ(validateStoredLicense(tmp.db, &errorMessage),
|
||||
Result::Invalid);
|
||||
EXPECT_FALSE(errorMessage.empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user