BareGit

Implement API key

Author: MetroWind <chris.corsair@gmail.com>
Date: Mon Sep 21 13:46:58 2026 -0700
Commit: 9f4f914e7c0d375db13841cb26291e2862a5ca69

Changes

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 136444b..203f44a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -33,6 +33,7 @@ FetchContent_Declare(
 set(SPDLOG_USE_STD_FORMAT ON)
 set(LIBMW_BUILD_URL ON)
 set(LIBMW_BUILD_HTTP_SERVER ON)
+set(LIBMW_BUILD_SQLITE ON)
 FetchContent_MakeAvailable(libmw spdlog json cxxopts)
 
 add_executable(telegrammer src/main.cpp)
@@ -41,6 +42,7 @@ target_link_libraries(telegrammer PRIVATE
     mw::mw 
     mw::http-server 
     mw::url 
+    mw::sqlite
     nlohmann_json::nlohmann_json 
     spdlog::spdlog
     cxxopts
diff --git a/README.md b/README.md
index f842b11..316f420 100644
--- a/README.md
+++ b/README.md
@@ -46,10 +46,27 @@ Run the executable from the build directory. You must provide your Telegram Bot
 | `-t, --token` | **Required.** Your Telegram Bot Token. | |
 | `-p, --port` | Port to listen on. | `8080` |
 | `-h, --host` | Interface to bind to. | `0.0.0.0` |
+| `--db` | Path to the SQLite database file. | `telegrammer.db` |
+| `--add-key <name>` | Generate and add a new API key for `<name>`. | |
+| `--delete-key <name>` | Delete the API key for `<name>`. | |
+| `--list-keys` | List all registered API keys. | |
 | `--help` | Show help message. | |
 
+## API Key Management
+
+Before using the API, you must generate an API key:
+
+```bash
+./telegrammer --add-key my_service
+# Output: Added key for 'my_service': YOUR_GENERATED_KEY
+```
+
 ## API Reference
 
+**Authentication:**
+All API requests must include the `Authorization` header with a valid API key:
+`Authorization: Bearer YOUR_GENERATED_KEY`
+
 ### 1. Send Message (`POST /send`)
 
 Send a text message to a chat. You can target a user by `chat_id` OR `username`.
diff --git a/prd.md b/prd.md
index 204fff1..2474014 100644
--- a/prd.md
+++ b/prd.md
@@ -7,5 +7,10 @@ A Telegram bot written in C++ that act as an API.
   Whenever the bot see a message in the chat, it POSTs the info of the
   message to the programs HTTP callback endpoint.
 * Use Cmake for build. Use [libmw](https://github.com/MetroWind/libmw)
-  for HTTP server and HTTP client, and various tools. You can find an
-  example project using libmw at @shrt.
+  for HTTP server and HTTP client, sqlite access, and various tools.
+  You can find an example project using libmw at @shrt.
+* The API will need an api key as authentication. The API keys (and
+  name) are stored in a sqlite database.
+* There should be a set of command line parameters to add, delete, and
+  list API keys. When a user wants to add an API key, they also need
+  to supply a name for the key.
diff --git a/src/main.cpp b/src/main.cpp
index a69389d..0530e94 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -7,6 +7,8 @@
 #include <chrono>
 #include <expected>
 #include <optional>
+#include <random>
+#include <filesystem>
 
 #include <cxxopts.hpp>
 #include <nlohmann/json.hpp>
@@ -16,9 +18,112 @@
 #include <mw/url.hpp>
 #include <mw/error.hpp>
 #include <mw/utils.hpp>
+#include <mw/database.hpp>
 
 using json = nlohmann::json;
 
+class ApiKeyManager
+{
+public:
+    explicit ApiKeyManager(const std::string& db_path)
+    {
+        auto db_res = mw::SQLite::connectFile(db_path);
+        if(!db_res.has_value())
+        {
+            throw std::runtime_error("Failed to connect to DB: " +
+                                     mw::errorMsg(db_res.error()));
+        }
+        db_ = std::move(*db_res);
+
+        auto create_res = db_->execute(
+            "CREATE TABLE IF NOT EXISTS api_keys ("
+            "name TEXT PRIMARY KEY, "
+            "key TEXT NOT NULL, "
+            "created_at INTEGER DEFAULT (unixepoch())"
+            ");");
+        if(!create_res.has_value())
+        {
+            throw std::runtime_error("Failed to create table: " +
+                                     mw::errorMsg(create_res.error()));
+        }
+    }
+
+    mw::E<std::string> addKey(const std::string& name)
+    {
+        std::string key = generateKey();
+        auto stmt = db_->statementFromStr(
+            "INSERT INTO api_keys (name, key) VALUES (?, ?);");
+        if(!stmt.has_value()) return std::unexpected(stmt.error());
+
+        auto bind_res = stmt->bind(name, key);
+        if(!bind_res.has_value()) return std::unexpected(bind_res.error());
+
+        auto exec_res = db_->execute(std::move(*stmt));
+        if(!exec_res.has_value()) return std::unexpected(exec_res.error());
+
+        return key;
+    }
+
+    mw::E<void> deleteKey(const std::string& name)
+    {
+        auto stmt = db_->statementFromStr(
+            "DELETE FROM api_keys WHERE name = ?;");
+        if(!stmt.has_value()) return std::unexpected(stmt.error());
+
+        auto bind_res = stmt->bind(name);
+        if(!bind_res.has_value()) return std::unexpected(bind_res.error());
+
+        return db_->execute(std::move(*stmt));
+    }
+
+    mw::E<std::vector<std::pair<std::string, std::string>>> listKeys()
+    {
+        return db_->eval<std::string, std::string>(
+                   "SELECT name, key FROM api_keys;")
+            .transform([](auto rows)
+            {
+                std::vector<std::pair<std::string, std::string>> keys;
+                for(const auto& row : rows)
+                {
+                    keys.emplace_back(std::get<0>(row), std::get<1>(row));
+                }
+                return keys;
+            });
+    }
+
+    bool isValid(const std::string& key)
+    {
+        auto stmt = db_->statementFromStr(
+            "SELECT COUNT(*) FROM api_keys WHERE key = ?;");
+        if(!stmt.has_value()) return false;
+        stmt->bind(key);
+        auto res = db_->evalToValue<int>(std::move(*stmt));
+        return res.has_value() && *res > 0;
+    }
+
+private:
+    std::unique_ptr<mw::SQLite> db_;
+
+    std::string generateKey()
+    {
+        static const char alphanum[] =
+            "0123456789"
+            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+            "abcdefghijklmnopqrstuvwxyz";
+        std::random_device rd;
+        std::mt19937 gen(rd());
+        std::uniform_int_distribution<> dis(0, sizeof(alphanum) - 2);
+
+        std::string key;
+        key.reserve(32);
+        for(int i = 0; i < 32; ++i)
+        {
+            key += alphanum[dis(gen)];
+        }
+        return key;
+    }
+};
+
 class TelegramClient
 {
 public:
@@ -113,15 +218,24 @@ private:
 class App : public mw::HTTPServer
 {
 public:
-    App(mw::IPSocketInfo listen_info, std::string token)
+    App(mw::IPSocketInfo listen_info, std::string token,
+        std::shared_ptr<ApiKeyManager> key_manager)
         : mw::HTTPServer(listen_info),
-          tg_client_(std::move(token))
+          tg_client_(std::move(token)),
+          key_manager_(std::move(key_manager))
     {}
 
     void setup()
     {
         server.Post("/send", [this](const Request& req, Response& res)
         {
+            if(!checkAuth(req))
+            {
+                res.status = 401;
+                res.set_content("Unauthorized", "text/plain");
+                return;
+            }
+
             try
             {
                 auto body = json::parse(req.body);
@@ -173,6 +287,13 @@ public:
 
         server.Post("/subscribe", [this](const Request& req, Response& res)
         {
+            if(!checkAuth(req))
+            {
+                res.status = 401;
+                res.set_content("Unauthorized", "text/plain");
+                return;
+            }
+
             try
             {
                 auto body = json::parse(req.body);
@@ -231,6 +352,18 @@ public:
     }
 
 private:
+    bool checkAuth(const Request& req)
+    {
+        if(!req.has_header("Authorization")) return false;
+        std::string auth = req.get_header_value("Authorization");
+        if(auth.starts_with("Bearer "))
+        {
+            std::string key = auth.substr(7);
+            return key_manager_->isValid(key);
+        }
+        return false;
+    }
+
     void dispatchMessage(const json& message)
     {
         int64_t chat_id = message["chat"]["id"];
@@ -264,6 +397,7 @@ private:
     TelegramClient tg_client_;
     SubscriptionManager sub_manager_;
     UsernameResolver username_resolver_;
+    std::shared_ptr<ApiKeyManager> key_manager_;
     bool running_ = true;
 };
 
@@ -276,6 +410,13 @@ int main(int argc, char** argv)
          cxxopts::value<int>()->default_value("8080"))
         ("h,host", "Interface to bind to",
          cxxopts::value<std::string>()->default_value("0.0.0.0"))
+        ("db", "Path to database file",
+         cxxopts::value<std::string>()->default_value("telegrammer.db"))
+        ("add-key", "Add a new API key with name",
+         cxxopts::value<std::string>())
+        ("delete-key", "Delete an API key by name",
+         cxxopts::value<std::string>())
+        ("list-keys", "List all API keys")
         ("help", "Print help");
 
     try
@@ -288,9 +429,68 @@ int main(int argc, char** argv)
             return 0;
         }
 
+        std::string db_path = result["db"].as<std::string>();
+        auto key_manager = std::make_shared<ApiKeyManager>(db_path);
+
+        if(result.count("add-key"))
+        {
+            std::string name = result["add-key"].as<std::string>();
+            auto key = key_manager->addKey(name);
+            if(key.has_value())
+            {
+                std::cout << "Added key for '" << name << "': " << *key
+                          << std::endl;
+                return 0;
+            }
+            else
+            {
+                spdlog::error("Failed to add key: {}",
+                              mw::errorMsg(key.error()));
+                return 1;
+            }
+        }
+
+        if(result.count("delete-key"))
+        {
+            std::string name = result["delete-key"].as<std::string>();
+            auto res = key_manager->deleteKey(name);
+            if(res.has_value())
+            {
+                std::cout << "Deleted key for '" << name << "'" << std::endl;
+                return 0;
+            }
+            else
+            {
+                spdlog::error("Failed to delete key: {}",
+                              mw::errorMsg(res.error()));
+                return 1;
+            }
+        }
+
+        if(result.count("list-keys"))
+        {
+            auto keys = key_manager->listKeys();
+            if(keys.has_value())
+            {
+                std::cout << "API Keys:" << std::endl;
+                for(const auto& [name, key] : *keys)
+                {
+                    std::cout << "- " << name << ": " << key << std::endl;
+                }
+                return 0;
+            }
+            else
+            {
+                spdlog::error("Failed to list keys: {}",
+                              mw::errorMsg(keys.error()));
+                return 1;
+            }
+        }
+
         if(!result.count("token"))
         {
-            spdlog::error("Token is required. Use --token <TOKEN>");
+            spdlog::error("Token is required to start server. "
+                          "Use --token <TOKEN>");
             std::cout << cmd_options.help() << std::endl;
             return 1;
         }
@@ -303,7 +503,7 @@ int main(int argc, char** argv)
         listen_info.address = host;
         listen_info.port = port;
 
-        App app(listen_info, token);
+        App app(listen_info, token, key_manager);
         app.setup();
 
         auto start_res = app.start();
@@ -328,11 +528,11 @@ int main(int argc, char** argv)
             polling_thread.join();
         }
     }
-    catch(const cxxopts::exceptions::exception& e)
+    catch(const std::exception& e)
     {
-        spdlog::error("Error parsing options: {}", e.what());
+        spdlog::error("Error: {}", e.what());
         return 1;
     }
 
     return 0;
-}
+}
\ No newline at end of file
diff --git a/test_api.sh b/test_api.sh
index b79bd00..ef325b2 100644
--- a/test_api.sh
+++ b/test_api.sh
@@ -2,17 +2,37 @@
 
 # Simple test script for Telegrammer API
 # NOTE: This requires Telegrammer to be running.
+# Usage: ./test_api.sh <API_KEY>
+# If no key provided, it assumes you've added one manually or disabled auth (not possible anymore).
 
 API_URL="http://localhost:8080"
+API_KEY="$1"
+
+if [ -z "$API_KEY" ]; then
+    echo "Error: API Key required."
+    echo "Usage: $0 <API_KEY>"
+    exit 1
+fi
+
+echo "Using API Key: $API_KEY"
 
 echo "Testing /subscribe..."
 curl -X POST "$API_URL/subscribe" \
      -H "Content-Type: application/json" \
+     -H "Authorization: Bearer $API_KEY" \
      -d '{"chat_id": 12345, "callback_url": "http://localhost:9090/webhook"}'
 echo -e "\n"
 
+echo "Testing /send (should fail if token is invalid, but test API structure)..."
+curl -X POST "$API_URL/send" \
+     -H "Content-Type: application/json" \
+     -H "Authorization: Bearer $API_KEY" \
+     -d '{"chat_id": 12345, "text": "Test message"}'
+echo -e "\n"
+
 echo "Testing /send with username (requires prior message from user)..."
 curl -X POST "$API_URL/send" \
      -H "Content-Type: application/json" \
+     -H "Authorization: Bearer $API_KEY" \
      -d '{"username": "some_user", "text": "Test message to username"}'
-echo -e "\n"
+echo -e "\n"
\ No newline at end of file