- Voice enhancements, user management, payment method

This commit is contained in:
Ty Clifford
2026-06-08 15:44:15 -04:00
parent 9e3ff61eb7
commit 063b8dc3e8
26 changed files with 2832 additions and 927 deletions
+53 -29
View File
@@ -11,9 +11,11 @@
define('ROOT_DIR', __DIR__);
define('CONFIG_FILE', ROOT_DIR . '/config.json');
define('ADMIN_VERSION', '1.0.0');
require_once ROOT_DIR . '/bootstrap.php';
require_once ROOT_DIR . '/lib/admin_platform.php';
// ─── CHANGE THIS PASSWORD ────────────────────────────────────────────────────
define('ADMIN_PASSWORD', 'changeme123');
define('ADMIN_PASSWORD', (string)getConfigVal('admin.password', 'changeme123'));
// ─────────────────────────────────────────────────────────────────────────────
session_name('cyberchat_admin');
@@ -34,21 +36,14 @@ function cfgVal(string $path, mixed $default = null): mixed {
}
function db(): PDO {
static $d = null;
if ($d !== null) return $d;
$path = ROOT_DIR . '/' . ltrim(cfgVal('database.main_db', 'db/chat.sqlite'), '/');
if (!file_exists($path)) die('<div class="alert err">Database not found at: ' . esc($path) . '</div>');
$d = new PDO('sqlite:' . $path);
$d->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$d->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
ensureAdminMessageColumns($d);
return $d;
return getDB();
}
function ensureAdminMessageColumns(PDO $d): void {
$columns = [];
foreach ($d->query("PRAGMA table_info(messages)") as $column) $columns[$column['name']] = true;
foreach ([
'group_id' => 'INTEGER',
'message_type' => "TEXT NOT NULL DEFAULT 'text'",
'voice_file' => 'TEXT',
'voice_mime' => 'TEXT',
@@ -60,7 +55,7 @@ function ensureAdminMessageColumns(PDO $d): void {
function archiveDB(string $year, string $month, string $day): ?PDO {
$tpl = cfgVal('database.archive_path', 'archive/{year}/{month}/{day}.sqlite');
$path = ROOT_DIR . '/' . ltrim(str_replace(['{year}','{month}','{day}'], [$year,$month,$day], $tpl), '/');
$path = storagePath(str_replace(['{year}','{month}','{day}'], [$year,$month,$day], $tpl));
if (!file_exists($path)) return null;
$a = new PDO('sqlite:' . $path);
$a->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
@@ -71,11 +66,9 @@ function archiveDB(string $year, string $month, string $day): ?PDO {
function createArchiveDB(string $year, string $month, string $day): PDO {
$tpl = cfgVal('database.archive_path', 'archive/{year}/{month}/{day}.sqlite');
$path = ROOT_DIR . '/' . ltrim(str_replace(['{year}','{month}','{day}'], [$year,$month,$day], $tpl), '/');
$path = storagePath(str_replace(['{year}','{month}','{day}'], [$year,$month,$day], $tpl));
$dir = dirname($path);
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
throw new RuntimeException('Could not create archive directory.');
}
ensureWritableDirectory($dir, 'Archive');
$a = new PDO('sqlite:' . $path);
$a->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
@@ -83,6 +76,7 @@ function createArchiveDB(string $year, string $month, string $day): PDO {
$a->exec("CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
group_id INTEGER,
username TEXT NOT NULL,
color TEXT NOT NULL,
message TEXT NOT NULL,
@@ -133,7 +127,7 @@ function csrfCheck(): void {
function deleteRecording(?string $filename): void {
if (!$filename || basename($filename) !== $filename) return;
$dir = ROOT_DIR . '/' . trim((string)cfgVal('voice.upload_dir', 'uploads/voice'), '/');
$dir = storagePath((string)cfgVal('voice.upload_dir', 'uploads/voice'));
$path = $dir . '/' . $filename;
if (is_file($path)) unlink($path);
}
@@ -149,7 +143,7 @@ function voiceFileUrl(?string $filename): string {
function voiceFileSize(?string $filename): ?int {
if (!$filename || basename($filename) !== $filename) return null;
$path = ROOT_DIR . '/' . trim((string)cfgVal('voice.upload_dir', 'uploads/voice'), '/') . '/' . $filename;
$path = storagePath((string)cfgVal('voice.upload_dir', 'uploads/voice')) . '/' . $filename;
return is_file($path) ? filesize($path) : null;
}
@@ -186,6 +180,16 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
csrfCheck();
$act = $_POST['action'] ?? '';
if (in_array($act, ['save_platform_services','save_plans','create_plan','run_mysql_mirror'], true)) {
try {
$message = handleAdminPlatformAction($act);
flash($message ?: 'Platform action completed.');
} catch (Throwable $e) {
flash($e->getMessage(), 'err');
}
redirect('tab=platform');
}
// ── Messages ──────────────────────────────────────────────────────────────
if ($act === 'archive_voice_clip') {
$id = (int)($_POST['msg_id'] ?? 0);
@@ -216,10 +220,10 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
}
if ($existingFile === false) {
$insert = $adb->prepare("INSERT INTO messages
(id,user_id,username,color,message,message_type,voice_file,voice_mime,voice_duration,created_at,day_key)
VALUES (?,?,?,?,?,?,?,?,?,?,?)");
(id,user_id,group_id,username,color,message,message_type,voice_file,voice_mime,voice_duration,created_at,day_key)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
$insert->execute([
$row['id'], $row['user_id'], $row['username'], $row['color'], $row['message'],
$row['id'], $row['user_id'], $row['group_id'] ?? null, $row['username'], $row['color'], $row['message'],
$row['message_type'], $row['voice_file'], $row['voice_mime'], $row['voice_duration'],
$row['created_at'], $row['day_key']
]);
@@ -321,7 +325,7 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
$day = $_POST['day'];
[$y,$m,$d] = explode('-', $day);
$tpl = cfgVal('database.archive_path', 'archive/{year}/{month}/{day}.sqlite');
$path = ROOT_DIR . '/' . ltrim(str_replace(['{year}','{month}','{day}'], [$y,$m,$d], $tpl), '/');
$path = storagePath(str_replace(['{year}','{month}','{day}'], [$y,$m,$d], $tpl));
if (file_exists($path)) {
$adb = archiveDB($y, $m, $d);
if ($adb) deleteRecordingsForRows($adb->query("SELECT voice_file FROM messages")->fetchAll());
@@ -356,8 +360,9 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
if ($errors) { flash(implode(' ', $errors), 'err'); redirect('tab=users'); }
// Check username collision
$existing = db()->prepare("SELECT id FROM users WHERE username = ? COLLATE NOCASE AND id != ?")->execute([$username, $id]);
if (db()->query("SELECT COUNT(*) FROM users WHERE username = '$username' AND id != $id")->fetchColumn() > 0) {
$existing = db()->prepare("SELECT id FROM users WHERE username = ? COLLATE NOCASE AND id != ?");
$existing->execute([$username, $id]);
if ($existing->fetchColumn()) {
flash('Username already taken.', 'err'); redirect('tab=users');
}
@@ -403,8 +408,9 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
if ($errors) { flash(implode(' ', $errors), 'err'); redirect('tab=users'); }
try {
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => (int)cfgVal('security.bcrypt_cost', 10)]);
db()->prepare("INSERT INTO users (username,password_hash,color,created_at) VALUES (?,?,?,?)")
->execute([$username, $hash, $color, time()]);
$freeId = (int)db()->query("SELECT id FROM plans WHERE slug='free'")->fetchColumn();
db()->prepare("INSERT INTO users (username,password_hash,color,plan_id,created_at) VALUES (?,?,?,?,?)")
->execute([$username, $hash, $color, $freeId ?: null, time()]);
flash("User '$username' created.");
} catch (Exception $e) { flash('Username already exists.', 'err'); }
redirect('tab=users');
@@ -458,7 +464,7 @@ if ($isLoggedIn) {
// Archive list
$archives = [];
if ($isLoggedIn) {
$archDir = ROOT_DIR . '/' . cfgVal('archive.archive_dir', 'archive');
$archDir = storagePath((string)cfgVal('archive.archive_dir', 'archive'));
if (is_dir($archDir)) {
foreach (glob($archDir . '/*/*/*.sqlite') as $f) {
if (preg_match('/(\d{4})\/(\d{2})\/(\d{2})\.sqlite$/', $f, $m)) {
@@ -684,6 +690,17 @@ td.actions{white-space:nowrap;width:1px}
max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
font-family:var(--mono);font-size:11px;color:var(--t0);
}
.admin-audio{
display:block;width:280px;max-width:100%;height:34px;
color-scheme:dark;accent-color:var(--g);
border:1px solid rgba(0,255,159,.38);border-radius:2px;
background:linear-gradient(90deg,rgba(0,255,159,.08),rgba(0,184,255,.04)),var(--bg2);
box-shadow:inset 0 0 14px rgba(0,0,0,.55),0 0 8px rgba(0,255,159,.12);
}
.admin-audio::-webkit-media-controls-enclosure{border-radius:0;background:transparent}
.admin-audio::-webkit-media-controls-panel{background:linear-gradient(90deg,#07120f,#08111a)}
.admin-audio::-webkit-media-controls-current-time-display,
.admin-audio::-webkit-media-controls-time-remaining-display{color:var(--g);font-family:var(--mono);font-size:9px}
/* ─── Modal ──────────────────────────────────────────────────────────── */
.modal-bg{
@@ -845,6 +862,10 @@ td.actions{white-space:nowrap;width:1px}
<span class="ico">⚡</span><span>Sessions</span>
</a>
<a class="sb-link <?= $tab==='platform'?'active':'' ?>" href="?tab=platform">
<span class="ico">◆</span><span>Plans & Platform</span>
</a>
<div class="sb-section">SYSTEM</div>
<a class="sb-link <?= $tab==='config'?'active':'' ?>" href="?tab=config">
<span class="ico">⚙</span><span>Config</span>
@@ -1074,7 +1095,7 @@ td.actions{white-space:nowrap;width:1px}
<td>
<?php if (($msg['message_type'] ?? 'text') === 'voice' && !empty($msg['voice_file'])): ?>
<div class="badge badge-b" style="margin-bottom:4px">VOICE · <?= esc(round((float)$msg['voice_duration'], 1)) ?>s</div>
<audio controls preload="metadata" style="display:block;width:260px;height:30px"
<audio class="admin-audio" controls preload="metadata"
src="<?= esc(trim((string)cfgVal('voice.upload_dir', 'uploads/voice'), '/') . '/' . rawurlencode($msg['voice_file'])) ?>"></audio>
<?php else: ?>
<div class="msg-preview" title="<?= esc($msg['message']) ?>"><?= esc($msg['message']) ?></div>
@@ -1240,7 +1261,7 @@ td.actions{white-space:nowrap;width:1px}
<td style="color:<?= esc($clip['color']) ?>;font-family:var(--mono);font-size:11px;white-space:nowrap"><?= esc($clip['username']) ?></td>
<td>
<?php if ($clipUrl !== '' && $clipSize !== null): ?>
<audio controls preload="metadata" style="display:block;width:280px;height:30px" src="<?= esc($clipUrl) ?>"></audio>
<audio class="admin-audio" controls preload="metadata" src="<?= esc($clipUrl) ?>"></audio>
<?php else: ?>
<span class="badge badge-r">RECORDING MISSING</span>
<?php endif; ?>
@@ -1350,7 +1371,7 @@ td.actions{white-space:nowrap;width:1px}
<td>
<?php if (($msg['message_type'] ?? 'text') === 'voice' && !empty($msg['voice_file'])): ?>
<div class="badge badge-b" style="margin-bottom:4px">VOICE · <?= esc(round((float)$msg['voice_duration'], 1)) ?>s</div>
<audio controls preload="metadata" style="display:block;width:260px;height:30px"
<audio class="admin-audio" controls preload="metadata"
src="<?= esc(trim((string)cfgVal('voice.upload_dir', 'uploads/voice'), '/') . '/' . rawurlencode($msg['voice_file'])) ?>"></audio>
<?php else: ?>
<div class="msg-preview" title="<?= esc($msg['message']) ?>"><?= esc($msg['message']) ?></div>
@@ -1628,6 +1649,9 @@ td.actions{white-space:nowrap;width:1px}
</div>
<?php /* ════════════════════════════════ CONFIG ══════════════════════ */ ?>
<?php elseif ($tab === 'platform'): ?>
<?php renderAdminPlatformTab(); ?>
<?php elseif ($tab === 'config'): ?>
<div class="page-head">