243 lines
17 KiB
PHP
243 lines
17 KiB
PHP
<?php
|
|
|
|
function handleAdminPlatformAction(string $action): ?string {
|
|
if ($action === 'save_platform_services') {
|
|
setConfigValues([
|
|
'subscriptions.enabled' => !empty($_POST['subscriptions_enabled']),
|
|
'mailgun.api_key' => trim((string)($_POST['mailgun_api_key'] ?? '')),
|
|
'mailgun.domain' => trim((string)($_POST['mailgun_domain'] ?? '')),
|
|
'mailgun.from' => trim((string)($_POST['mailgun_from'] ?? '')),
|
|
'mailgun.api_base' => trim((string)($_POST['mailgun_api_base'] ?? 'https://api.mailgun.net/v3')),
|
|
'stripe.secret_key' => trim((string)($_POST['stripe_secret_key'] ?? '')),
|
|
'stripe.webhook_secret' => trim((string)($_POST['stripe_webhook_secret'] ?? '')),
|
|
'stripe.success_url' => trim((string)($_POST['stripe_success_url'] ?? '')),
|
|
'stripe.cancel_url' => trim((string)($_POST['stripe_cancel_url'] ?? '')),
|
|
'stripe.portal_return_url' => trim((string)($_POST['stripe_portal_return_url'] ?? '')),
|
|
'database.mysql.enabled' => !empty($_POST['mysql_enabled']),
|
|
'database.mysql.auto_import' => !empty($_POST['mysql_auto_import']),
|
|
'database.mysql.dsn' => trim((string)($_POST['mysql_dsn'] ?? '')),
|
|
'database.mysql.username' => trim((string)($_POST['mysql_username'] ?? '')),
|
|
'database.mysql.password' => (string)($_POST['mysql_password'] ?? ''),
|
|
'database.mysql.sync_interval_seconds' => max(60, (int)($_POST['mysql_sync_interval'] ?? 900)),
|
|
]);
|
|
return 'Platform service settings saved.';
|
|
}
|
|
|
|
if ($action === 'save_plans') {
|
|
$db = getDB();
|
|
$plans = $_POST['plans'] ?? [];
|
|
$boolFeatures = [
|
|
'voice_messages', 'recording_status', 'auto_voice_send', 'username_color',
|
|
'text_export', 'voice_archive', 'voice_export', 'email_2fa',
|
|
];
|
|
$db->beginTransaction();
|
|
try {
|
|
$update = $db->prepare("UPDATE plans SET name=?,description=?,price_cents=?,currency=?,
|
|
billing_interval=?,stripe_price_id=?,active=?,sort_order=?,updated_at=? WHERE id=?");
|
|
$upsertFeature = $db->prepare("INSERT INTO plan_features (plan_id,feature_key,value_json) VALUES (?,?,?)
|
|
ON CONFLICT(plan_id,feature_key) DO UPDATE SET value_json=excluded.value_json");
|
|
foreach ($plans as $id => $plan) {
|
|
$id = (int)$id;
|
|
$name = trim((string)($plan['name'] ?? ''));
|
|
if ($id < 1 || $name === '') continue;
|
|
$update->execute([
|
|
$name,
|
|
trim((string)($plan['description'] ?? '')),
|
|
max(0, (int)($plan['price_cents'] ?? 0)),
|
|
strtolower(substr(trim((string)($plan['currency'] ?? 'usd')), 0, 3)),
|
|
in_array($plan['billing_interval'] ?? '', ['day', 'week', 'month', 'year'], true)
|
|
? $plan['billing_interval'] : 'month',
|
|
trim((string)($plan['stripe_price_id'] ?? '')) ?: null,
|
|
!empty($plan['active']) ? 1 : 0,
|
|
(int)($plan['sort_order'] ?? 0),
|
|
time(),
|
|
$id,
|
|
]);
|
|
foreach ($boolFeatures as $key) {
|
|
$upsertFeature->execute([$id, $key, json_encode(!empty($plan['features'][$key]))]);
|
|
}
|
|
$upsertFeature->execute([$id, 'group_member_limit', json_encode(max(2, min(100, (int)($plan['features']['group_member_limit'] ?? 2))))]);
|
|
$upsertFeature->execute([$id, 'text_archive_days', json_encode(max(30, min(3650, (int)($plan['features']['text_archive_days'] ?? 30))))]);
|
|
}
|
|
$db->commit();
|
|
} catch (Throwable $e) {
|
|
if ($db->inTransaction()) $db->rollBack();
|
|
throw $e;
|
|
}
|
|
return 'Plans, prices, limits, and feature assignments saved.';
|
|
}
|
|
|
|
if ($action === 'create_plan') {
|
|
$slug = strtolower(trim((string)($_POST['slug'] ?? '')));
|
|
$name = trim((string)($_POST['name'] ?? ''));
|
|
if (!preg_match('/^[a-z0-9_-]{2,30}$/', $slug) || $name === '') {
|
|
throw new RuntimeException('Plan slug or name is invalid.');
|
|
}
|
|
$db = getDB();
|
|
$db->prepare("INSERT INTO plans
|
|
(slug,name,description,price_cents,currency,billing_interval,active,sort_order,created_at,updated_at)
|
|
VALUES (?,?,?,0,'usd','month',1,50,?,?)")
|
|
->execute([$slug, $name, trim((string)($_POST['description'] ?? '')), time(), time()]);
|
|
$id = (int)$db->lastInsertId();
|
|
$features = [
|
|
'voice_messages' => true, 'recording_status' => false, 'auto_voice_send' => false,
|
|
'group_member_limit' => 2, 'username_color' => false, 'text_archive_days' => 30,
|
|
'text_export' => true, 'voice_archive' => false, 'voice_export' => false, 'email_2fa' => false,
|
|
];
|
|
$insert = $db->prepare("INSERT INTO plan_features (plan_id,feature_key,value_json) VALUES (?,?,?)");
|
|
foreach ($features as $key => $value) $insert->execute([$id, $key, json_encode($value)]);
|
|
return "Plan '$name' created.";
|
|
}
|
|
|
|
if ($action === 'run_mysql_mirror') {
|
|
require_once ROOT_DIR . '/lib/mysql_mirror.php';
|
|
$result = mirrorAllSQLiteDatabases();
|
|
return "MySQL mirror completed: {$result['databases']} database(s), {$result['rows']} row(s).";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function adminFeatureLabel(string $key): string {
|
|
return ucwords(str_replace('_', ' ', $key));
|
|
}
|
|
|
|
function renderAdminPlatformTab(): void {
|
|
$plans = plans(false);
|
|
$db = getDB();
|
|
$subscribers = $db->query("SELECT u.id,u.username,u.email,u.email_verified_at,u.subscription_status,
|
|
u.subscription_period_end,u.cancel_at_period_end,p.name AS plan_name,u.stripe_customer_id
|
|
FROM users u LEFT JOIN plans p ON p.id=u.plan_id
|
|
ORDER BY (u.subscription_status IN ('active','trialing','past_due')) DESC,u.created_at DESC LIMIT 500")->fetchAll();
|
|
$stats = [
|
|
'visits_24h' => (int)$db->query("SELECT COUNT(*) FROM visitor_events WHERE event_type='visit' AND created_at>" . (time() - 86400))->fetchColumn(),
|
|
'unique_24h' => (int)$db->query("SELECT COUNT(DISTINCT ip_hash) FROM visitor_events WHERE created_at>" . (time() - 86400))->fetchColumn(),
|
|
'paid' => (int)$db->query("SELECT COUNT(*) FROM users WHERE subscription_status IN ('active','trialing','past_due')")->fetchColumn(),
|
|
'groups' => (int)$db->query("SELECT COUNT(*) FROM chat_groups")->fetchColumn(),
|
|
];
|
|
$events = $db->query("SELECT event_type,COUNT(*) AS total FROM visitor_events
|
|
WHERE created_at>" . (time() - 2592000) . " GROUP BY event_type ORDER BY total DESC")->fetchAll();
|
|
$featureKeys = [
|
|
'voice_messages', 'recording_status', 'auto_voice_send', 'group_member_limit',
|
|
'username_color', 'text_archive_days', 'text_export', 'voice_archive',
|
|
'voice_export', 'email_2fa',
|
|
];
|
|
?>
|
|
<div class="page-head"><h1>PLANS & PLATFORM</h1>
|
|
<p>// pricing, entitlements, subscriptions, email, Stripe, database mirroring, and usage</p></div>
|
|
|
|
<div class="stat-grid">
|
|
<div class="stat-card green"><div class="stat-num"><?= $stats['visits_24h'] ?></div><div class="stat-label">Visits / 24h</div></div>
|
|
<div class="stat-card blue"><div class="stat-num"><?= $stats['unique_24h'] ?></div><div class="stat-label">Unique / 24h</div></div>
|
|
<div class="stat-card violet"><div class="stat-num"><?= $stats['paid'] ?></div><div class="stat-label">Paid Subscribers</div></div>
|
|
<div class="stat-card orange"><div class="stat-num"><?= $stats['groups'] ?></div><div class="stat-label">Private Groups</div></div>
|
|
</div>
|
|
|
|
<form method="post">
|
|
<input type="hidden" name="csrf" value="<?= csrfToken() ?>">
|
|
<input type="hidden" name="action" value="save_platform_services">
|
|
<div class="panel"><div class="panel-head"><span class="panel-title">// SUBSCRIPTION VISIBILITY</span></div>
|
|
<div class="panel-body"><label class="cb-row"><input type="checkbox" name="subscriptions_enabled" <?= getConfigVal('subscriptions.enabled', true) ? 'checked' : '' ?>>
|
|
<span>Allow and display new subscriptions. When off, existing subscribers can still manage billing.</span></label></div></div>
|
|
|
|
<div class="panel"><div class="panel-head"><span class="panel-title">// MAILGUN</span></div><div class="panel-body">
|
|
<div class="form-row3">
|
|
<div class="field"><label>API Key</label><input type="password" name="mailgun_api_key" value="<?= esc(getConfigVal('mailgun.api_key', '')) ?>"></div>
|
|
<div class="field"><label>Domain</label><input name="mailgun_domain" value="<?= esc(getConfigVal('mailgun.domain', '')) ?>"></div>
|
|
<div class="field"><label>From</label><input name="mailgun_from" value="<?= esc(getConfigVal('mailgun.from', '')) ?>"></div>
|
|
</div>
|
|
<div class="field"><label>API Base</label><input name="mailgun_api_base" value="<?= esc(getConfigVal('mailgun.api_base', 'https://api.mailgun.net/v3')) ?>"></div>
|
|
</div></div>
|
|
|
|
<div class="panel"><div class="panel-head"><span class="panel-title">// STRIPE</span></div><div class="panel-body">
|
|
<div class="form-row">
|
|
<div class="field"><label>Secret Key</label><input type="password" name="stripe_secret_key" value="<?= esc(getConfigVal('stripe.secret_key', '')) ?>"></div>
|
|
<div class="field"><label>Webhook Secret</label><input type="password" name="stripe_webhook_secret" value="<?= esc(getConfigVal('stripe.webhook_secret', '')) ?>"></div>
|
|
</div>
|
|
<div class="form-row3">
|
|
<div class="field"><label>Success URL</label><input name="stripe_success_url" value="<?= esc(getConfigVal('stripe.success_url', '')) ?>"></div>
|
|
<div class="field"><label>Cancel URL</label><input name="stripe_cancel_url" value="<?= esc(getConfigVal('stripe.cancel_url', '')) ?>"></div>
|
|
<div class="field"><label>Portal Return URL</label><input name="stripe_portal_return_url" value="<?= esc(getConfigVal('stripe.portal_return_url', '')) ?>"></div>
|
|
</div>
|
|
<p class="dim">Webhook endpoint: <span class="mono"><?= esc(appBaseUrl() . '/api/stripe-webhook.php') ?></span></p>
|
|
</div></div>
|
|
|
|
<div class="panel"><div class="panel-head"><span class="panel-title">// OPTIONAL MYSQL / MARIADB MIRROR</span></div><div class="panel-body">
|
|
<label class="cb-row"><input type="checkbox" name="mysql_enabled" <?= getConfigVal('database.mysql.enabled', false) ? 'checked' : '' ?>><span>Enable MySQL mirror</span></label>
|
|
<label class="cb-row"><input type="checkbox" name="mysql_auto_import" <?= getConfigVal('database.mysql.auto_import', false) ? 'checked' : '' ?>><span>Automatically import all SQLite databases on the configured interval</span></label>
|
|
<div class="form-row3">
|
|
<div class="field"><label>PDO DSN</label><input name="mysql_dsn" value="<?= esc(getConfigVal('database.mysql.dsn', '')) ?>" placeholder="mysql:host=localhost;dbname=cyberchat;charset=utf8mb4"></div>
|
|
<div class="field"><label>Username</label><input name="mysql_username" value="<?= esc(getConfigVal('database.mysql.username', '')) ?>"></div>
|
|
<div class="field"><label>Password</label><input type="password" name="mysql_password" value="<?= esc(getConfigVal('database.mysql.password', '')) ?>"></div>
|
|
</div>
|
|
<div class="field"><label>Sync Interval Seconds</label><input type="number" min="60" name="mysql_sync_interval" value="<?= (int)getConfigVal('database.mysql.sync_interval_seconds', 900) ?>"></div>
|
|
</div></div>
|
|
<button class="btn btn-g" type="submit">SAVE PLATFORM SERVICES</button>
|
|
</form>
|
|
|
|
<div class="panel" style="margin-top:20px"><div class="panel-head"><span class="panel-title">// PLANS, PRICES & ENTITLEMENTS</span></div>
|
|
<div class="panel-body"><form method="post">
|
|
<input type="hidden" name="csrf" value="<?= csrfToken() ?>"><input type="hidden" name="action" value="save_plans">
|
|
<?php foreach ($plans as $plan): ?>
|
|
<div class="danger-zone" style="border-color:var(--bd2);margin:0 0 14px">
|
|
<div class="form-row3">
|
|
<div class="field"><label>Name / <?= esc($plan['slug']) ?></label><input name="plans[<?= $plan['id'] ?>][name]" value="<?= esc($plan['name']) ?>"></div>
|
|
<div class="field"><label>Price Cents</label><input type="number" min="0" name="plans[<?= $plan['id'] ?>][price_cents]" value="<?= $plan['price_cents'] ?>"></div>
|
|
<div class="field"><label>Stripe Price ID</label><input name="plans[<?= $plan['id'] ?>][stripe_price_id]" value="<?= esc($plan['stripe_price_id']) ?>" placeholder="price_..."></div>
|
|
</div>
|
|
<div class="field"><label>Description</label><input name="plans[<?= $plan['id'] ?>][description]" value="<?= esc($plan['description']) ?>"></div>
|
|
<div class="form-row3">
|
|
<div class="field"><label>Currency</label><input maxlength="3" name="plans[<?= $plan['id'] ?>][currency]" value="<?= esc($plan['currency']) ?>"></div>
|
|
<div class="field"><label>Interval</label><select name="plans[<?= $plan['id'] ?>][billing_interval]">
|
|
<?php foreach (['day','week','month','year'] as $interval): ?><option <?= $plan['billing_interval']===$interval?'selected':'' ?>><?= $interval ?></option><?php endforeach; ?>
|
|
</select></div>
|
|
<div class="field"><label>Sort Order</label><input type="number" name="plans[<?= $plan['id'] ?>][sort_order]" value="<?= (int)$plan['sort_order'] ?>"></div>
|
|
</div>
|
|
<label class="cb-row"><input type="checkbox" name="plans[<?= $plan['id'] ?>][active]" <?= $plan['active'] ? 'checked' : '' ?>><span>Plan active</span></label>
|
|
<div class="form-row3">
|
|
<?php foreach ($featureKeys as $key): ?>
|
|
<?php if (in_array($key, ['group_member_limit','text_archive_days'], true)): ?>
|
|
<div class="field"><label><?= esc(adminFeatureLabel($key)) ?></label><input type="number"
|
|
min="<?= $key==='text_archive_days' ? 30 : 2 ?>"
|
|
name="plans[<?= $plan['id'] ?>][features][<?= $key ?>]" value="<?= (int)($plan['features'][$key] ?? 0) ?>"></div>
|
|
<?php else: ?>
|
|
<label class="cb-row"><input type="checkbox" name="plans[<?= $plan['id'] ?>][features][<?= $key ?>]"
|
|
<?= !empty($plan['features'][$key]) ? 'checked' : '' ?>><span><?= esc(adminFeatureLabel($key)) ?></span></label>
|
|
<?php endif; ?>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<button class="btn btn-g" type="submit">SAVE ALL PLANS</button>
|
|
</form></div>
|
|
</div>
|
|
|
|
<div class="panel"><div class="panel-head"><span class="panel-title">// CREATE TIER</span></div><div class="panel-body">
|
|
<form method="post"><input type="hidden" name="csrf" value="<?= csrfToken() ?>"><input type="hidden" name="action" value="create_plan">
|
|
<div class="form-row3"><div class="field"><label>Slug</label><input name="slug" placeholder="business"></div>
|
|
<div class="field"><label>Name</label><input name="name" placeholder="Business"></div>
|
|
<div class="field"><label>Description</label><input name="description"></div></div>
|
|
<button class="btn btn-b" type="submit">CREATE TIER</button>
|
|
</form></div></div>
|
|
|
|
<div class="panel"><div class="panel-head"><span class="panel-title">// SUBSCRIBER TRACKING</span></div><div class="panel-body np">
|
|
<div class="tbl-wrap"><table><thead><tr><th>User</th><th>Email</th><th>Plan</th><th>Status</th><th>Period End</th><th>Stripe</th></tr></thead><tbody>
|
|
<?php foreach ($subscribers as $subscriber): ?><tr>
|
|
<td><?= esc($subscriber['username']) ?></td>
|
|
<td class="dim"><?= esc($subscriber['email'] ?: 'not set') ?> <?= $subscriber['email_verified_at'] ? '<span class="badge badge-g">VERIFIED</span>' : '' ?></td>
|
|
<td><?= esc($subscriber['plan_name'] ?: 'Free') ?></td>
|
|
<td><span class="badge <?= in_array($subscriber['subscription_status'], ['active','trialing'], true) ? 'badge-g' : 'badge-o' ?>"><?= esc(strtoupper($subscriber['subscription_status'])) ?></span></td>
|
|
<td class="dim"><?= $subscriber['subscription_period_end'] ? esc(fmtTime((int)$subscriber['subscription_period_end'])) : '-' ?><?= $subscriber['cancel_at_period_end'] ? ' / CANCELING' : '' ?></td>
|
|
<td class="dim"><?= esc($subscriber['stripe_customer_id'] ?: '-') ?></td>
|
|
</tr><?php endforeach; ?></tbody></table></div>
|
|
</div></div>
|
|
|
|
<div class="panel"><div class="panel-head"><span class="panel-title">// 30-DAY EVENT TOTALS</span></div><div class="panel-body">
|
|
<div class="btn-group"><?php foreach ($events as $event): ?><span class="badge badge-b"><?= esc($event['event_type']) ?>: <?= (int)$event['total'] ?></span><?php endforeach; ?></div>
|
|
</div></div>
|
|
|
|
<form method="post"><input type="hidden" name="csrf" value="<?= csrfToken() ?>"><input type="hidden" name="action" value="run_mysql_mirror">
|
|
<button class="btn btn-v" type="submit">RUN MYSQL MIRROR NOW</button></form>
|
|
<?php
|
|
}
|