feat: free-tier credit system + Syttende Mai access for Google users
- FreeTier.php: credit check/deduct/reset engine with hourly rate limit - bootstrap.php: dbnmDb() singleton, dbnToolsIsFreeTier(), credit gate helpers - index.php: store tier=free|approved in session from SSO JWT - All 7 API endpoints: credit gate (402/429) + X-Credits-Remaining header - layout.php: credit meta tag, JS balance var, Syttende Mai banner (05-17 only) - tools.js: credit badge in topbar, 402 modal, 429 toast, dbnUpdateCredits() - barnevernet.js + deep-research.js: wire 402/429 handling for NDJSON streams - tools.css: styles for credit badge, no-credits modal, rate-limit toast Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -385,6 +385,103 @@ function dbnToolsRagDb(): PDO
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PDO connection to dobetternorge_maindb (user accounts + credits).
|
||||
* Credentials come from DBNM_DB_* env vars.
|
||||
*/
|
||||
function dbnmDb(): PDO
|
||||
{
|
||||
static $pdo = null;
|
||||
if ($pdo !== null) {
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
$host = dbnToolsEnv('DBNM_DB_HOST', 'localhost');
|
||||
$name = dbnToolsEnv('DBNM_DB_NAME', 'dobetternorge_maindb');
|
||||
$user = dbnToolsEnv('DBNM_DB_USER', 'root');
|
||||
$pass = dbnToolsEnv('DBNM_DB_PASS', '');
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$host};dbname={$name};charset=utf8mb4",
|
||||
$user,
|
||||
$pass,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
throw new DbnToolsHttpException('Credit database is not reachable.', 503, 'credit_db_unavailable');
|
||||
}
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the current session is a free-tier SSO user (Google login).
|
||||
* False for CaveauAI client sessions (always unlimited).
|
||||
*/
|
||||
function dbnToolsIsFreeTier(): bool
|
||||
{
|
||||
return !empty($_SESSION['dbn_tools_authenticated'])
|
||||
&& !empty($_SESSION['dbn_tools_sso_uid'])
|
||||
&& ($_SESSION['dbn_tools_tier'] ?? '') === 'free';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce free-tier credit gate before a tool call.
|
||||
* Exits with JSON 402/429 if the user is over limit or out of credits.
|
||||
* No-op for CaveauAI sessions.
|
||||
*
|
||||
* @return int The user_id for use in dbnToolsFreeTierDeduct()
|
||||
*/
|
||||
function dbnToolsFreeTierCheck(string $tool): int
|
||||
{
|
||||
if (!dbnToolsIsFreeTier()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/FreeTier.php';
|
||||
$uid = (int)$_SESSION['dbn_tools_sso_uid'];
|
||||
$result = FreeTier::check($uid, $tool);
|
||||
|
||||
if (!$result['ok']) {
|
||||
$isRateLimit = ($result['reason'] ?? '') === 'rate_limit';
|
||||
http_response_code($isRateLimit ? 429 : 402);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => ['code' => $result['reason'], 'message' => $isRateLimit
|
||||
? 'Rate limit reached — you can make up to 10 requests per hour on the free tier.'
|
||||
: 'No credits remaining. Your 10 free credits reset on the 1st of next month.',
|
||||
],
|
||||
'balance' => $result['balance'],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
return $uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduct credits after a successful tool call.
|
||||
* The $uid returned from dbnToolsFreeTierCheck() must be passed in.
|
||||
* No-op when uid === 0 (non-free-tier session).
|
||||
*
|
||||
* @return int Remaining balance (for response header)
|
||||
*/
|
||||
function dbnToolsFreeTierDeduct(int $uid, string $tool): int
|
||||
{
|
||||
if ($uid === 0) {
|
||||
return -1;
|
||||
}
|
||||
require_once __DIR__ . '/FreeTier.php';
|
||||
return FreeTier::deduct($uid, $tool);
|
||||
}
|
||||
|
||||
function dbnToolsClientSlug(): string
|
||||
{
|
||||
return dbnToolsEnv('DBN_CAVEAU_CLIENT_SLUG') ?: 'dobetter';
|
||||
|
||||
Reference in New Issue
Block a user