feat: require an api key to schedule

This commit is contained in:
2026-06-08 15:54:05 +01:00
parent a0bff2beb1
commit 458b7897dd
5 changed files with 53 additions and 3 deletions
+13
View File
@@ -61,3 +61,16 @@ function error_out(string $message, int $status = 400): never
{ {
json_out(['error' => $message], $status); json_out(['error' => $message], $status);
} }
/**
* Aborts with 400 if no Clickatell API key has been configured.
*/
function require_api_key(): void
{
$stmt = db()->prepare("SELECT setting_value FROM `system` WHERE setting_name = 'clickatell_api_key'");
$stmt->execute();
$key = $stmt->fetchColumn();
if ($key === false || trim((string) $key) === '') {
error_out('No Clickatell API key is configured. Set one in Admin → Clickatell Settings before sending or scheduling messages.', 400);
}
}
+4
View File
@@ -42,6 +42,8 @@ function handle_get(): never
function handle_post(): never function handle_post(): never
{ {
require_api_key();
$body = json_body(); $body = json_body();
$templateId = (int) ($body['template_id'] ?? 0); $templateId = (int) ($body['template_id'] ?? 0);
$mode = (string) ($body['mode'] ?? ''); $mode = (string) ($body['mode'] ?? '');
@@ -104,6 +106,8 @@ function handle_post(): never
function handle_put(): never function handle_put(): never
{ {
require_api_key();
$id = (int) ($_GET['id'] ?? 0); $id = (int) ($_GET['id'] ?? 0);
if ($id <= 0) { if ($id <= 0) {
error_out('Missing or invalid id'); error_out('Missing or invalid id');
+2
View File
@@ -16,6 +16,8 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
// } // }
try { try {
require_api_key();
$body = json_body(); $body = json_body();
$customBody = trim((string) ($body['message'] ?? '')); $customBody = trim((string) ($body['message'] ?? ''));
$templateId = isset($body['template_id']) ? (int) $body['template_id'] : null; $templateId = isset($body['template_id']) ? (int) $body['template_id'] : null;
+16 -1
View File
@@ -117,6 +117,11 @@
<i class="fa-solid fa-flask me-1"></i> Sandbox mode is active — texts are tagged as a test and excluded from <i class="fa-solid fa-flask me-1"></i> Sandbox mode is active — texts are tagged as a test and excluded from
cost calculations. cost calculations.
</div> </div>
<div id="noApiKeyBanner" class="alert alert-danger"
style="display:none;font-size:0.85rem;font-weight:600;text-align:center;">
<i class="fa-solid fa-circle-exclamation me-1"></i> No Clickatell API key configured —
<a href="index.html" class="alert-link">set one in Admin settings</a> before scheduling.
</div>
<div class="page-header"> <div class="page-header">
<h1>Schedule</h1> <h1>Schedule</h1>
@@ -161,7 +166,7 @@
<option value="" disabled selected>Loading templates…</option> <option value="" disabled selected>Loading templates…</option>
</select> </select>
<button class="btn btn-primary" onclick="addSchedule()"> <button class="btn btn-primary" id="addScheduleBtn" onclick="addSchedule()">
<i class="fa-solid fa-plus me-1"></i> Add to Schedule <i class="fa-solid fa-plus me-1"></i> Add to Schedule
</button> </button>
</div> </div>
@@ -516,6 +521,16 @@
} }
checkAuth(); checkAuth();
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') checkAuth(); }); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') checkAuth(); });
async function checkApiKey() {
try {
const r = await fetch('/api/settings.php?key=clickatell_api_key').then(res => res.json());
const missing = !r.set;
document.getElementById('noApiKeyBanner').style.display = missing ? '' : 'none';
document.getElementById('addScheduleBtn').disabled = missing;
} catch { }
}
checkApiKey();
</script> </script>
</body> </body>
+18 -2
View File
@@ -103,6 +103,11 @@
<i class="fa-solid fa-flask me-1"></i> Sandbox mode is active — texts are tagged as a test and excluded from <i class="fa-solid fa-flask me-1"></i> Sandbox mode is active — texts are tagged as a test and excluded from
cost calculations. cost calculations.
</div> </div>
<div id="noApiKeyBanner" class="alert alert-danger"
style="display:none;font-size:0.85rem;font-weight:600;text-align:center;">
<i class="fa-solid fa-circle-exclamation me-1"></i> No Clickatell API key configured —
<a href="index.html" class="alert-link">set one in Admin settings</a> before sending.
</div>
<div class="page-header d-flex align-items-center gap-3"> <div class="page-header d-flex align-items-center gap-3">
<a href="index.html" class="btn btn-sm btn-secondary"> <a href="index.html" class="btn btn-sm btn-secondary">
@@ -196,6 +201,7 @@
const countSpan = document.getElementById('recipientCount'); const countSpan = document.getElementById('recipientCount');
const recipientsSel = document.getElementById('recipients'); const recipientsSel = document.getElementById('recipients');
const resultEl = document.getElementById('sendResult'); const resultEl = document.getElementById('sendResult');
let noApiKey = false;
// ── Populate recipients from API ─────────────────────────── // ── Populate recipients from API ───────────────────────────
async function loadRecipients() { async function loadRecipients() {
@@ -276,7 +282,7 @@
sectionEl.style.visibility = empty ? 'hidden' : 'visible'; sectionEl.style.visibility = empty ? 'hidden' : 'visible';
placeholderEl.style.display = empty ? '' : 'none'; placeholderEl.style.display = empty ? '' : 'none';
previewEl.textContent = empty ? '' : previewText(val); previewEl.textContent = empty ? '' : previewText(val);
sendBtn.disabled = empty; sendBtn.disabled = empty || noApiKey;
resultEl.style.display = 'none'; resultEl.style.display = 'none';
}); });
@@ -333,7 +339,7 @@
<i class="fa-solid fa-circle-xmark me-2"></i>${esc(err.message)} <i class="fa-solid fa-circle-xmark me-2"></i>${esc(err.message)}
</div>`; </div>`;
} finally { } finally {
sendBtn.disabled = msgEl.value.trim() === ''; sendBtn.disabled = msgEl.value.trim() === '' || noApiKey;
sendBtn.innerHTML = '<i class="fa-solid fa-paper-plane me-2"></i>Send to <span id="recipientCount">' + countSpan.textContent + '</span> attendees'; sendBtn.innerHTML = '<i class="fa-solid fa-paper-plane me-2"></i>Send to <span id="recipientCount">' + countSpan.textContent + '</span> attendees';
} }
}); });
@@ -384,6 +390,16 @@
} }
checkAuth(); checkAuth();
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') checkAuth(); }); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') checkAuth(); });
async function checkApiKey() {
try {
const r = await fetch('/api/settings.php?key=clickatell_api_key').then(res => res.json());
noApiKey = !r.set;
document.getElementById('noApiKeyBanner').style.display = noApiKey ? '' : 'none';
if (noApiKey) document.getElementById('sendBtn').disabled = true;
} catch { }
}
checkApiKey();
</script> </script>
</body> </body>