feat(dashboard): scoped API keys with rate limits and quotas

Bearer keys (dlk_...) are checked per request against their owner's
current account (ban, lock, demotion, sign out everywhere stop or narrow
them at once) and their scope: permission routes need the permission in
scope, read-only keys only read, level-only routes need an all-permission
key, and session-only paths (sign-in, password, 2FA, key management)
are never reachable with a key. Per-key rate limit and daily quota with
429 and X-RateLimit/X-Quota/Retry-After headers; usage is written in
batches every minute. WebSocket subscriptions honour the scope too.

Routes to list, make, rotate and revoke keys; staff with
api_keys_manage can see and revoke others' keys under the rank rules.
POST /api/auth/token now makes an all-permission key. Audit entries for
create/rotate/revoke/denied, and actions done with a key are attributed
to "user (key name)".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 08:43:12 -05:00
parent 03d5fae0c5
commit fc4e4eda9f
30 changed files with 1194 additions and 33 deletions

View File

@@ -0,0 +1,92 @@
#pragma once
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <unordered_map>
/**
* Per API key request limits: a rate (requests a minute, as a token bucket that refills evenly, so a key can burst up
* to its whole minute and then goes at the steady rate) and an optional quota of requests per UTC day. Only accepted
* requests use up the rate and the quota. Kept in memory; the day count is seeded from the database when a key is
* first seen, so a restart doesn't hand out a fresh quota.
*/
class ApiKeyLimiter {
public:
using Clock = std::chrono::steady_clock;
struct Decision {
bool allowed{};
bool quotaExceeded{}; // refused by the daily quota rather than the rate
uint32_t limit{}; // requests a minute
uint32_t remaining{}; // left in the current minute's allowance
uint32_t retryAfterSeconds{}; // when refused
uint32_t quota{}; // requests a day, 0: none
uint32_t quotaRemaining{};
uint32_t dayCount{}; // accepted requests today, after this one
};
/**
* @param day The UTC day (days since 1970) of the request
* @param secondsToNextDay Seconds until that day ends (for Retry-After once the quota is used up)
* @param storedDay, storedDayCount What the database last recorded for the key, used when it is first seen
*/
Decision Check(uint64_t keyId, uint32_t perMinute, uint32_t dailyQuota, int32_t day, uint32_t secondsToNextDay,
int32_t storedDay, uint32_t storedDayCount, Clock::time_point now) {
perMinute = std::max<uint32_t>(perMinute, 1);
auto [it, inserted] = m_Keys.try_emplace(keyId);
auto& state = it->second;
if (inserted) {
state.tokens = perMinute;
state.refilledAt = now;
state.day = storedDay;
state.dayCount = storedDay == day ? storedDayCount : 0;
}
if (state.day != day) {
state.day = day;
state.dayCount = 0;
}
// Refill at perMinute a minute, up to one minute's worth (a lowered limit takes effect at once)
const double elapsed = std::chrono::duration<double>(now - state.refilledAt).count();
state.tokens = std::min<double>(perMinute, state.tokens + elapsed * perMinute / 60.0);
state.refilledAt = now;
Decision decision;
decision.limit = perMinute;
decision.quota = dailyQuota;
if (dailyQuota > 0 && state.dayCount >= dailyQuota) {
decision.quotaExceeded = true;
decision.retryAfterSeconds = std::max<uint32_t>(secondsToNextDay, 1);
} else if (state.tokens < 1.0) {
decision.retryAfterSeconds = static_cast<uint32_t>((1.0 - state.tokens) * 60.0 / perMinute) + 1;
} else {
state.tokens -= 1.0;
state.dayCount++;
decision.allowed = true;
}
decision.remaining = static_cast<uint32_t>(state.tokens);
decision.dayCount = state.dayCount;
decision.quotaRemaining = dailyQuota > state.dayCount ? dailyQuota - state.dayCount : 0;
return decision;
}
// A revoked or rotated key starts over
void Forget(uint64_t keyId) { m_Keys.erase(keyId); }
// Drop keys not used for a while, so memory stays bounded
void Prune(Clock::time_point now, std::chrono::seconds idle) {
std::erase_if(m_Keys, [&](const auto& entry) { return now - entry.second.refilledAt > idle; });
}
size_t Size() const { return m_Keys.size(); }
private:
struct State {
double tokens{};
Clock::time_point refilledAt;
int32_t day{};
uint32_t dayCount{};
};
std::unordered_map<uint64_t, State> m_Keys;
};

View File

@@ -0,0 +1,284 @@
#include "ApiKeyService.h"
#include <chrono>
#include <ctime>
#include <map>
#include <mutex>
#include <unordered_map>
#include <openssl/rand.h>
#include "ApiKeyLimiter.h"
#include "Database.h"
#include "DashboardAuthService.h"
#include "dConfig.h"
#include "Game.h"
#include "GeneralUtils.h"
#include "HTTPContext.h"
#include "HTTPReply.h"
#include "Logger.h"
namespace {
using Clock = std::chrono::steady_clock;
// How long what the database says about a key is trusted. Revoking and rotating on this server take effect at
// once (Forget); the owner's account is looked up on every request regardless.
constexpr auto CACHE_TTL = std::chrono::seconds(30);
constexpr size_t CACHE_MAX = 10000;
constexpr auto FLUSH_INTERVAL = std::chrono::seconds(60);
constexpr auto DENIED_THROTTLE = std::chrono::seconds(60);
constexpr auto LIMITER_IDLE = std::chrono::minutes(30);
struct CachedKey {
std::optional<IApiKeys::ApiKey> key; // nullopt: no such key (so a stream of bad keys doesn't hit the database)
std::shared_ptr<const ApiKeys::Scope> scope;
std::vector<std::string> allowedIps;
std::vector<std::string> allowedPaths;
Clock::time_point loadedAt;
};
struct State {
std::recursive_mutex mutex;
std::unordered_map<std::string, CachedKey> cache; // by key hash
ApiKeyLimiter limiter;
std::map<uint64_t, IApiKeys::ApiKeyUsage> pending;
std::map<std::pair<uint64_t, std::string>, Clock::time_point> deniedAt;
Clock::time_point nextFlush = Clock::now() + FLUSH_INTERVAL;
std::function<std::string(const HTTPContext&)> clientAddress;
std::function<void(const HTTPContext&, const std::string&)> deniedHook;
};
State& GetState() {
static State state;
return state;
}
const CachedKey& Lookup(State& state, const std::string& hash) {
const auto now = Clock::now();
auto it = state.cache.find(hash);
if (it != state.cache.end() && now - it->second.loadedAt < CACHE_TTL) return it->second;
if (state.cache.size() >= CACHE_MAX) state.cache.clear();
CachedKey entry;
entry.loadedAt = now;
entry.key = Database::Get()->GetApiKeyByHash(hash);
if (entry.key) {
auto scope = std::make_shared<ApiKeys::Scope>();
scope->keyId = entry.key->id;
scope->name = entry.key->name;
scope->readOnly = entry.key->readOnly;
ApiKeys::ParsePermissions(entry.key->permissions, scope->allPermissions, scope->permissions);
entry.scope = std::move(scope);
entry.allowedIps = ApiKeys::SplitList(entry.key->allowedIps);
entry.allowedPaths = ApiKeys::SplitList(entry.key->allowedPaths);
}
return state.cache[hash] = std::move(entry);
}
struct Owner {
std::string username;
uint8_t gmLevel{};
};
// The key is in use and its owner may still sign in; the owner's GM level as of now
std::optional<Owner> CheckKeyAndOwner(const IApiKeys::ApiKey& key) {
const auto now = static_cast<int64_t>(std::time(nullptr));
if (key.revokedAt != 0 || (key.expiresAt != 0 && key.expiresAt <= now)) return std::nullopt;
const auto account = Database::Get()->GetAccountById(key.accountId);
if (account.contains("error") || account.value("banned", false) || account.value("locked", false)) return std::nullopt;
const auto gmLevel = static_cast<uint8_t>(account.value("gm_level", 0));
if (!DashboardAuthService::HasDashboardAccess(gmLevel)) return std::nullopt;
// "Sign out everywhere" and password resets stop keys made before them, as they stop sessions
if (key.issuedAt < Database::Get()->GetSessionsValidAfter(key.accountId)) return std::nullopt;
return Owner{ account.value("name", std::string{}), gmLevel };
}
std::string Hex(const unsigned char* bytes, size_t size) {
static constexpr char digits[] = "0123456789abcdef";
std::string out;
out.reserve(size * 2);
for (size_t i = 0; i < size; ++i) {
out += digits[bytes[i] >> 4];
out += digits[bytes[i] & 15];
}
return out;
}
void Refuse(HTTPReply& reply, eHTTPStatusCode status, const std::string& message) {
reply.status = status;
reply.contentType = eContentType::APPLICATION_JSON;
reply.message = nlohmann::json{ {"success", false}, {"error", message} }.dump();
}
}
namespace ApiKeyService {
NewSecret GenerateSecret() {
unsigned char bytes[32];
if (RAND_bytes(bytes, sizeof(bytes)) != 1) throw std::runtime_error("RAND_bytes failed");
NewSecret secret;
secret.token = std::string(ApiKeys::TOKEN_PREFIX) + Hex(bytes, sizeof(bytes));
secret.hash = DashboardAuthService::Sha256Hex(secret.token);
secret.prefix = secret.token.substr(0, ApiKeys::TOKEN_PREFIX.size() + 6);
return secret;
}
bool LooksLikeKey(std::string_view token) {
return token.starts_with(ApiKeys::TOKEN_PREFIX);
}
bool SessionOnlyPath(std::string_view path) {
if (path.starts_with("/api/auth/")) return true;
if (path.starts_with("/api/account/") && path != "/api/account/permissions") return true;
return path.find("/api_keys") != std::string_view::npos;
}
uint32_t DefaultRateLimit() {
if (!Game::config) return DEFAULT_RATE_LIMIT;
const auto value = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("api_key_rate_limit"));
return value && *value > 0 ? std::min(*value, MAX_RATE_LIMIT) : DEFAULT_RATE_LIMIT;
}
eResult Authenticate(const std::string& token, HTTPContext& context, HTTPReply& reply) {
auto& state = GetState();
std::lock_guard lock(state.mutex);
const auto& cached = Lookup(state, DashboardAuthService::Sha256Hex(token));
if (!cached.key) return eResult::INVALID;
const auto& key = *cached.key;
const auto owner = CheckKeyAndOwner(key);
if (!owner) return eResult::INVALID;
context.isAuthenticated = true;
context.authenticatedUser = owner->username;
context.accountId = key.accountId;
context.gmLevel = owner->gmLevel;
context.apiKey = cached.scope;
context.userData["auth_source"] = "header";
context.userData["api_key"] = key.name;
if (DashboardAuthService::NeedsTwoFactorSetup(key.accountId, owner->gmLevel)) context.userData["needs_2fa"] = "1";
const auto address = state.clientAddress ? state.clientAddress(context) : context.clientIP;
if (!ApiKeys::AddressAllowed(cached.allowedIps, address)) {
NoteDenied(context, "address " + address + " isn't allowed");
Refuse(reply, eHTTPStatusCode::FORBIDDEN, "This API key can't be used from this address");
return eResult::REFUSED;
}
if (SessionOnlyPath(context.path)) {
NoteDenied(context, context.method + " " + context.path + " needs a signed-in browser session");
Refuse(reply, eHTTPStatusCode::FORBIDDEN, "API keys can't be used for this; sign in to the dashboard instead");
return eResult::REFUSED;
}
if (!ApiKeys::PathAllowed(cached.allowedPaths, context.path)) {
NoteDenied(context, context.method + " " + context.path + " is outside its allowed paths");
Refuse(reply, eHTTPStatusCode::FORBIDDEN, "This API key isn't allowed to use this path");
return eResult::REFUSED;
}
const auto now = std::time(nullptr);
const auto day = static_cast<int32_t>(now / 86400);
const auto toNextDay = static_cast<uint32_t>(86400 - now % 86400);
const auto perMinute = key.rateLimit > 0 ? std::min(key.rateLimit, MAX_RATE_LIMIT) : DefaultRateLimit();
const auto decision = state.limiter.Check(key.id, perMinute, key.dailyQuota, day, toNextDay, key.quotaDay, key.dayCount, Clock::now());
reply.headers.push_back("X-RateLimit-Limit: " + std::to_string(decision.limit));
reply.headers.push_back("X-RateLimit-Remaining: " + std::to_string(decision.remaining));
if (decision.quota > 0) {
reply.headers.push_back("X-Quota-Limit: " + std::to_string(decision.quota));
reply.headers.push_back("X-Quota-Remaining: " + std::to_string(decision.quotaRemaining));
}
if (!decision.allowed) {
reply.headers.push_back("Retry-After: " + std::to_string(decision.retryAfterSeconds));
NoteDenied(context, decision.quotaExceeded ? "its daily quota is used up" : "it went over its rate limit");
Refuse(reply, eHTTPStatusCode::TOO_MANY_REQUESTS, decision.quotaExceeded
? "This API key has used up its daily quota" : "This API key is making too many requests; slow down");
return eResult::REFUSED;
}
auto& usage = state.pending[key.id];
usage.id = key.id;
usage.requests++;
usage.lastUsedAt = now;
usage.lastIp = address.substr(0, 64);
usage.quotaDay = day;
usage.dayCount = decision.dayCount;
return eResult::OK;
}
std::optional<Verified> Verify(const std::string& token) {
auto& state = GetState();
std::lock_guard lock(state.mutex);
const auto& cached = Lookup(state, DashboardAuthService::Sha256Hex(token));
if (!cached.key) return std::nullopt;
const auto owner = CheckKeyAndOwner(*cached.key);
if (!owner) return std::nullopt;
return Verified{ cached.key->accountId, owner->username, owner->gmLevel,
DashboardAuthService::NeedsTwoFactorSetup(cached.key->accountId, owner->gmLevel), cached.scope };
}
void Forget(uint64_t keyId) {
auto& state = GetState();
std::lock_guard lock(state.mutex);
std::erase_if(state.cache, [keyId](const auto& entry) { return entry.second.key && entry.second.key->id == keyId; });
state.limiter.Forget(keyId);
}
void ForgetAll() {
auto& state = GetState();
std::lock_guard lock(state.mutex);
state.cache.clear();
}
std::optional<IApiKeys::ApiKeyUsage> PendingUsage(uint64_t keyId) {
auto& state = GetState();
std::lock_guard lock(state.mutex);
const auto it = state.pending.find(keyId);
if (it == state.pending.end()) return std::nullopt;
return it->second;
}
void Flush() {
auto& state = GetState();
std::lock_guard lock(state.mutex);
state.nextFlush = Clock::now() + FLUSH_INTERVAL;
if (state.pending.empty()) return;
std::vector<IApiKeys::ApiKeyUsage> batch;
batch.reserve(state.pending.size());
for (const auto& [_, usage] : state.pending) batch.push_back(usage);
try {
Database::Get()->RecordApiKeyUsage(batch);
state.pending.clear();
} catch (const std::exception& ex) {
LOG("Failed to save API key usage: %s", ex.what());
}
}
void Update() {
auto& state = GetState();
std::lock_guard lock(state.mutex);
const auto now = Clock::now();
if (now < state.nextFlush) return;
Flush();
state.limiter.Prune(now, LIMITER_IDLE);
std::erase_if(state.deniedAt, [&](const auto& entry) { return now - entry.second >= DENIED_THROTTLE; });
}
void SetClientAddress(std::function<std::string(const HTTPContext&)> resolve) {
GetState().clientAddress = std::move(resolve);
}
void SetDeniedHook(std::function<void(const HTTPContext&, const std::string& reason)> hook) {
GetState().deniedHook = std::move(hook);
}
void NoteDenied(const HTTPContext& context, const std::string& reason) {
if (!context.apiKey) return;
auto& state = GetState();
std::lock_guard lock(state.mutex);
const auto now = Clock::now();
auto [it, inserted] = state.deniedAt.try_emplace({ context.apiKey->keyId, reason }, now);
if (!inserted) {
if (now - it->second < DENIED_THROTTLE) return;
it->second = now;
}
LOG("API key %llu (%s) of %s refused: %s", static_cast<unsigned long long>(context.apiKey->keyId), context.apiKey->name.c_str(),
context.authenticatedUser.c_str(), reason.c_str());
if (state.deniedHook) state.deniedHook(context, reason);
}
}

View File

@@ -0,0 +1,77 @@
#pragma once
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include "ApiKeyScope.h"
#include "IApiKeys.h"
struct HTTPContext;
struct HTTPReply;
/**
* Dashboard API keys: checking them on requests, their rate limits and quotas, and their usage counters.
* A key's effective permissions are its scope AND its owner's current permissions: the owner is looked up on every
* request, so a ban, lock, demotion or "sign out everywhere" narrows or stops the key at once.
* Runs on the web thread only.
*/
namespace ApiKeyService {
constexpr uint32_t DEFAULT_RATE_LIMIT = 120; // requests a minute, unless api_key_rate_limit says otherwise
constexpr uint32_t MAX_RATE_LIMIT = 6000;
constexpr uint32_t MAX_DAILY_QUOTA = 10'000'000;
struct NewSecret {
std::string token; // shown to the person once, never stored
std::string hash; // SHA-256 hex, stored
std::string prefix; // the start of the key, stored to tell keys apart
};
NewSecret GenerateSecret();
bool LooksLikeKey(std::string_view token);
// Paths a key may never use, whatever its scope: signing in, the account's own password, email, two-factor and
// sessions, and managing API keys (a leaked key must not be able to make itself new keys or lock its owner out)
bool SessionOnlyPath(std::string_view path);
enum class eResult : uint8_t {
INVALID, // unknown, revoked or expired key, or an owner who can't sign in: the request is unauthenticated
OK, // context filled in
REFUSED, // reply written (403 for the key's IP/path restrictions, 429 for its limits)
};
eResult Authenticate(const std::string& token, HTTPContext& context, HTTPReply& reply);
// For WebSocket connections: the owner and scope, without counting against the limits
struct Verified {
uint32_t accountId{};
std::string username;
uint8_t gmLevel{};
bool needsTwoFactorSetup{};
std::shared_ptr<const ApiKeys::Scope> scope;
};
std::optional<Verified> Verify(const std::string& token);
// Stop using what is cached about a key (after it is revoked or rotated)
void Forget(uint64_t keyId);
void ForgetAll();
// Requests since the last write, not yet in the database (for the key list)
std::optional<IApiKeys::ApiKeyUsage> PendingUsage(uint64_t keyId);
// Write the usage counters now and then (call every tick) and before shutting down
void Update();
void Flush();
// The address a request came from, for the key's allowed addresses (the dashboard's behind_proxy rule)
void SetClientAddress(std::function<std::string(const HTTPContext&)> resolve);
// Record that a key was refused something; at most once a minute per key and reason, so a busy script can't flood
// the audit log
void SetDeniedHook(std::function<void(const HTTPContext&, const std::string& reason)> hook);
void NoteDenied(const HTTPContext& context, const std::string& reason);
uint32_t DefaultRateLimit();
}

View File

@@ -1,4 +1,5 @@
#include "AuthTokenHandler.h"
#include "ApiKeyService.h"
#include "DashboardAuthService.h"
#include "Game.h"
#include "Logger.h"
@@ -77,6 +78,13 @@ bool AuthTokenHandler::ProcessHTTPContext(HTTPContext& context, HTTPReply& reply
const auto token = ExtractToken(context.GetHeader("Cookie"), context.GetHeader("Authorization"), source);
if (token.empty()) return true;
// API keys: their scope and limits are checked here; a key over its limits is refused outright
if (source == eTokenSource::HEADER && ApiKeyService::LooksLikeKey(token)) {
const auto keyResult = ApiKeyService::Authenticate(token, context, reply);
if (keyResult == ApiKeyService::eResult::INVALID) LOG_DEBUG("API key validation failed from %s", context.clientIP.c_str());
return keyResult != ApiKeyService::eResult::REFUSED;
}
const auto result = ValidateToken(token);
if (!result.isValid) {
LOG_DEBUG("Authentication token validation failed: %s", result.errorMessage.c_str());

View File

@@ -4,6 +4,7 @@ set(DASHBOARDAUTH_SOURCES
"DashboardAuthService.cpp"
"AuthMiddleware.cpp"
"AuthTokenHandler.cpp"
"ApiKeyService.cpp"
"RequireAuthMiddleware.cpp"
)

View File

@@ -19,6 +19,15 @@ namespace {
std::function<bool(uint8_t)> g_ApiAccessAllowed;
std::function<void(const HTTPContext&, HTTPReply&)> g_ForbiddenPage;
std::function<void(const HTTPContext&, const std::string&)> g_ApiKeyDenied;
bool RefuseApiKey(const HTTPContext& context, HTTPReply& reply, const std::string& reason, const std::string& message) {
if (g_ApiKeyDenied) g_ApiKeyDenied(context, reason);
reply.status = eHTTPStatusCode::FORBIDDEN;
reply.message = nlohmann::json{ {"success", false}, {"error", message}, {"code", "api_key_scope"} }.dump();
reply.contentType = eContentType::APPLICATION_JSON;
return false;
}
bool IsSafeMethod(const HTTPContext& context) {
return context.method == "GET" || context.method == "HEAD" || context.method == "OPTIONS";
@@ -37,6 +46,13 @@ RequireAuthMiddleware::RequireAuthMiddleware(uint8_t minGmLevel) : requiredLevel
RequireAuthMiddleware::RequireAuthMiddleware(std::function<uint8_t()> requiredLevel) : requiredLevel(std::move(requiredLevel)) {}
RequireAuthMiddleware::RequireAuthMiddleware(std::function<uint8_t()> requiredLevel, std::string permission)
: requiredLevel(std::move(requiredLevel)), permission(std::move(permission)) {}
void RequireAuthMiddleware::SetApiKeyDeniedHook(std::function<void(const HTTPContext& context, const std::string& reason)> hook) {
g_ApiKeyDenied = std::move(hook);
}
bool RequireAuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
if (!context.isAuthenticated) {
LOG_DEBUG("Unauthorized access attempt to %s from %s", context.path.c_str(), context.clientIP.c_str());
@@ -105,5 +121,21 @@ bool RequireAuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
return false;
}
// An API key can only narrow what its owner may do: read-only keys make no changes, and a route guarded by a
// permission needs that permission in the key's scope. Routes guarded only by a GM level above 0 have no
// permission to scope them by, so only keys with all of the owner's permissions reach them.
if (context.apiKey) {
const auto& key = *context.apiKey;
if (key.readOnly && !readsOnly && !IsSafeMethod(context)) {
return RefuseApiKey(context, reply, context.method + " " + context.path + " with a read-only key", "This API key is read-only");
}
if (!permission.empty() && !key.Has(permission)) {
return RefuseApiKey(context, reply, "no " + permission + " permission for " + context.path, "This API key doesn't have the " + permission + " permission");
}
if (permission.empty() && minGmLevel > 0 && !key.allPermissions) {
return RefuseApiKey(context, reply, context.path + " needs a key with all permissions", "This needs an API key with all of your permissions");
}
}
return true;
}

View File

@@ -4,6 +4,7 @@
#include <memory>
#include <cstdint>
#include <functional>
#include <string>
#include "IHTTPMiddleware.h"
/**
@@ -22,6 +23,9 @@ public:
// The required level is looked up on every request (a permission that can be changed while running)
explicit RequireAuthMiddleware(std::function<uint8_t()> requiredLevel);
// A route guarded by a named permission: an API key must also have it in its scope
RequireAuthMiddleware(std::function<uint8_t()> requiredLevel, std::string permission);
~RequireAuthMiddleware() override = default;
bool Process(HTTPContext& context, HTTPReply& reply) override;
@@ -32,10 +36,18 @@ public:
// Renders the page a signed-in account gets when it may not open a page (not /api/); unset replies with JSON
static void SetForbiddenPage(std::function<void(const HTTPContext& context, HTTPReply& reply)> render);
// The route only reads, although it may be a POST (DataTables and lookups send their query as a body): read-only
// API keys may use it
void SetReadsOnly() { readsOnly = true; }
// Told when an API key is refused a route for its scope or because it is read-only (for the audit log)
static void SetApiKeyDeniedHook(std::function<void(const HTTPContext& context, const std::string& reason)> hook);
std::string GetName() const override { return "RequireAuthMiddleware"; }
private:
std::function<uint8_t()> requiredLevel;
std::string permission;
bool readsOnly{};
};
#endif // !__REQUIREAUTHMIDDLEWARE_H__