335 lines
26 KiB
PHP
Executable File
335 lines
26 KiB
PHP
Executable File
<?php
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 0);
|
|
|
|
function getDb() {
|
|
static $pdo = null;
|
|
if ($pdo === null) {
|
|
$pdo = new PDO('pgsql:host=localhost;dbname=adx_system', 'admin', 'admin123');
|
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
}
|
|
return $pdo;
|
|
}
|
|
|
|
function ensureTables($pdo) {
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.gsuite_config (key VARCHAR(100) PRIMARY KEY, value TEXT, updated_at TIMESTAMP DEFAULT NOW())");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.gsuite_domains (id SERIAL PRIMARY KEY, domain VARCHAR(255) UNIQUE, verified BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW())");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.gsuite_users (id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE, first_name VARCHAR(100), last_name VARCHAR(100), password VARCHAR(255), recovery_email VARCHAR(255), recovery_phone VARCHAR(50), domain VARCHAR(255), status VARCHAR(50) DEFAULT 'active', created_at TIMESTAMP DEFAULT NOW())");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.gsuite_operations (id SERIAL PRIMARY KEY, operation_type VARCHAR(50), status VARCHAR(20) DEFAULT 'pending', total_items INTEGER DEFAULT 0, processed_items INTEGER DEFAULT 0, success_count INTEGER DEFAULT 0, error_count INTEGER DEFAULT 0, details JSONB, created_at TIMESTAMP DEFAULT NOW())");
|
|
}
|
|
|
|
function generatePassword($length = 12) {
|
|
$chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
$password = '';
|
|
for ($i = 0; $i < $length; $i++) $password .= $chars[random_int(0, strlen($chars) - 1)];
|
|
return $password;
|
|
}
|
|
|
|
function generateSuffix($length = 4) {
|
|
$chars = 'abcdefghjkmnpqrstuvwxyz';
|
|
$suffix = '';
|
|
for ($i = 0; $i < $length; $i++) $suffix .= $chars[random_int(0, strlen($chars) - 1)];
|
|
return $suffix;
|
|
}
|
|
|
|
if (isset($_GET['action'])) {
|
|
header('Content-Type: application/json');
|
|
$pdo = getDb();
|
|
ensureTables($pdo);
|
|
|
|
switch ($_GET['action']) {
|
|
case 'stats':
|
|
$domains = $pdo->query("SELECT COUNT(*) FROM admin.gsuite_domains")->fetchColumn();
|
|
$users = $pdo->query("SELECT COUNT(*) FROM admin.gsuite_users")->fetchColumn();
|
|
$operations = $pdo->query("SELECT COUNT(*) FROM admin.gsuite_operations WHERE created_at > NOW() - INTERVAL '24 hours'")->fetchColumn();
|
|
echo json_encode(['domains' => (int)$domains, 'users' => (int)$users, 'operations_24h' => (int)$operations, 'api_connected' => false]);
|
|
break;
|
|
|
|
case 'list_domains':
|
|
$domains = $pdo->query("SELECT * FROM admin.gsuite_domains ORDER BY domain")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode($domains);
|
|
break;
|
|
|
|
case 'add_domain':
|
|
$domain = $_POST['domain'] ?? '';
|
|
if ($domain) {
|
|
$stmt = $pdo->prepare("INSERT INTO admin.gsuite_domains (domain) VALUES (?) ON CONFLICT (domain) DO NOTHING");
|
|
$stmt->execute([$domain]);
|
|
}
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'local_users':
|
|
$page = max(1, intval($_GET['page'] ?? 1));
|
|
$limit = 50;
|
|
$offset = ($page - 1) * $limit;
|
|
$domain = $_GET['domain'] ?? '';
|
|
$where = $domain ? "WHERE domain = " . $pdo->quote($domain) : "";
|
|
$users = $pdo->query("SELECT * FROM admin.gsuite_users $where ORDER BY created_at DESC LIMIT $limit OFFSET $offset")->fetchAll(PDO::FETCH_ASSOC);
|
|
$total = $pdo->query("SELECT COUNT(*) FROM admin.gsuite_users $where")->fetchColumn();
|
|
echo json_encode(['users' => $users, 'total' => (int)$total, 'page' => $page, 'pages' => ceil($total / $limit)]);
|
|
break;
|
|
|
|
case 'bulk_create':
|
|
$domain = $_POST['domain'] ?? '';
|
|
$namesJson = $_POST['names'] ?? '[]';
|
|
$names = json_decode($namesJson, true);
|
|
$count = min(intval($_POST['count'] ?? 20), count($names));
|
|
$emailFormat = $_POST['email_format'] ?? '{first}{last}{rand}';
|
|
$passwordType = $_POST['password_type'] ?? 'generate';
|
|
$fixedPassword = $_POST['fixed_password'] ?? '';
|
|
$recoveryEmail = $_POST['recovery_email'] ?? '';
|
|
$recoveryPhones = array_filter(explode(',', $_POST['recovery_phones'] ?? ''));
|
|
$phoneRotation = intval($_POST['phone_rotation'] ?? 5);
|
|
|
|
if (!$domain || empty($names)) {
|
|
echo json_encode(['error' => 'Domain and names required']);
|
|
break;
|
|
}
|
|
|
|
$stmt = $pdo->prepare("INSERT INTO admin.gsuite_operations (operation_type, total_items, details) VALUES ('bulk_create', ?, ?) RETURNING id");
|
|
$stmt->execute([$count, json_encode(['domain' => $domain])]);
|
|
$opId = $stmt->fetchColumn();
|
|
|
|
$results = [];
|
|
$phoneIndex = 0;
|
|
|
|
for ($i = 0; $i < $count && $i < count($names); $i++) {
|
|
$firstName = trim($names[$i]['first'] ?? '');
|
|
$lastName = trim($names[$i]['last'] ?? '');
|
|
if (!$firstName || !$lastName) continue;
|
|
|
|
$rand = generateSuffix();
|
|
$password = $passwordType === 'generate' ? generatePassword() : $fixedPassword;
|
|
$email = str_replace(['{first}', '{last}', '{rand}', '{f}', '{l}'], [strtolower($firstName), strtolower($lastName), $rand, strtolower($firstName[0]), strtolower($lastName[0])], $emailFormat) . '@' . $domain;
|
|
$phone = !empty($recoveryPhones) ? $recoveryPhones[$phoneIndex % count($recoveryPhones)] : '';
|
|
if ($phoneRotation > 0 && ($i + 1) % $phoneRotation === 0) $phoneIndex++;
|
|
|
|
$stmt = $pdo->prepare("INSERT INTO admin.gsuite_users (email, first_name, last_name, password, recovery_email, recovery_phone, domain) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (email) DO NOTHING");
|
|
$stmt->execute([$email, $firstName, $lastName, $password, $recoveryEmail, $phone, $domain]);
|
|
|
|
$results[] = ['email' => $email, 'password' => $password, 'recovery_phone' => $phone, 'success' => true];
|
|
$pdo->exec("UPDATE admin.gsuite_operations SET processed_items = processed_items + 1, success_count = success_count + 1 WHERE id = $opId");
|
|
}
|
|
|
|
$pdo->exec("UPDATE admin.gsuite_operations SET status = 'completed' WHERE id = $opId");
|
|
echo json_encode(['operation_id' => $opId, 'results' => $results]);
|
|
break;
|
|
|
|
case 'change_domain':
|
|
$source = $_POST['source_domain'] ?? '';
|
|
$target = $_POST['target_domain'] ?? '';
|
|
$limit = intval($_POST['limit'] ?? 100);
|
|
|
|
if (!$source || !$target) {
|
|
echo json_encode(['error' => 'Source and target domains required']);
|
|
break;
|
|
}
|
|
|
|
$users = $pdo->query("SELECT * FROM admin.gsuite_users WHERE domain = " . $pdo->quote($source) . " LIMIT $limit")->fetchAll(PDO::FETCH_ASSOC);
|
|
$results = [];
|
|
|
|
foreach ($users as $user) {
|
|
$oldEmail = $user['email'];
|
|
$localPart = explode('@', $oldEmail)[0];
|
|
$newEmail = $localPart . '@' . $target;
|
|
|
|
$stmt = $pdo->prepare("UPDATE admin.gsuite_users SET email = ?, domain = ? WHERE id = ?");
|
|
$stmt->execute([$newEmail, $target, $user['id']]);
|
|
|
|
$results[] = ['old_email' => $oldEmail, 'new_email' => $newEmail, 'success' => true];
|
|
}
|
|
|
|
echo json_encode(['results' => $results]);
|
|
break;
|
|
|
|
case 'delete_user':
|
|
$email = $_POST['email'] ?? '';
|
|
if ($email) $pdo->exec("DELETE FROM admin.gsuite_users WHERE email = " . $pdo->quote($email));
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'operations':
|
|
$ops = $pdo->query("SELECT * FROM admin.gsuite_operations ORDER BY created_at DESC LIMIT 20")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode($ops);
|
|
break;
|
|
|
|
case 'export_users':
|
|
$domain = $_GET['domain'] ?? '';
|
|
$where = $domain ? "WHERE domain = " . $pdo->quote($domain) : "";
|
|
$users = $pdo->query("SELECT email, password, recovery_phone, first_name, last_name, domain, status FROM admin.gsuite_users $where ORDER BY created_at DESC")->fetchAll(PDO::FETCH_ASSOC);
|
|
header('Content-Type: text/csv');
|
|
header('Content-Disposition: attachment; filename="gsuite_users.csv"');
|
|
$out = fopen('php://output', 'w');
|
|
fputcsv($out, ['Email', 'Password', 'Phone', 'FirstName', 'LastName', 'Domain', 'Status'], ';');
|
|
foreach ($users as $u) fputcsv($out, array_values($u), ';');
|
|
fclose($out);
|
|
exit;
|
|
|
|
default:
|
|
echo json_encode(['error' => 'Unknown action']);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
$pdo = getDb();
|
|
ensureTables($pdo);
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>GSuite Manager - WEVAL SEND</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
:root{--bg:#0a0a0f;--card:#12121a;--card2:#1a1a25;--border:#2a2a3a;--text:#e4e4e7;--text2:#9ca3af;--primary:#6366f1;--success:#10b981;--warning:#f59e0b;--danger:#ef4444;--google:#4285f4}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
|
.sidebar{position:fixed;left:0;top:0;bottom:0;width:200px;background:var(--card);border-right:1px solid var(--border);padding:20px 0}
|
|
.logo{padding:0 20px 20px;border-bottom:1px solid var(--border);margin-bottom:20px;font-size:16px;font-weight:700;display:flex;align-items:center;gap:10px}
|
|
.logo i{color:var(--google)}
|
|
.nav-item{display:flex;align-items:center;gap:10px;padding:12px 20px;color:var(--text2);cursor:pointer;font-size:13px}
|
|
.nav-item:hover,.nav-item.active{background:rgba(99,102,241,0.1);color:var(--text);border-left:3px solid var(--primary)}
|
|
.main{margin-left:200px;padding:20px}
|
|
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}
|
|
.header h1{font-size:22px}
|
|
.stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:24px}
|
|
.stat-card{background:var(--card);border-radius:12px;padding:20px;border:1px solid var(--border)}
|
|
.stat-value{font-size:28px;font-weight:700}
|
|
.stat-label{color:var(--text2);font-size:12px;margin-top:4px}
|
|
.panel{background:var(--card);border-radius:12px;border:1px solid var(--border);margin-bottom:20px;display:none}
|
|
.panel.active{display:block}
|
|
.panel-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
|
|
.panel-header h2{font-size:15px;display:flex;align-items:center;gap:8px}
|
|
.panel-body{padding:20px}
|
|
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
|
.form-group{margin-bottom:16px}
|
|
.form-group label{display:block;margin-bottom:6px;font-size:12px;color:var(--text2)}
|
|
.form-group input,.form-group select,.form-group textarea{width:100%;padding:10px;background:var(--card2);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px}
|
|
.form-group textarea{min-height:100px}
|
|
.btn{padding:10px 20px;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:12px;display:inline-flex;align-items:center;gap:8px}
|
|
.btn-primary{background:var(--primary);color:white}
|
|
.btn-success{background:var(--success);color:white}
|
|
.btn-warning{background:var(--warning);color:black}
|
|
.btn-danger{background:var(--danger);color:white}
|
|
.btn-google{background:var(--google);color:white}
|
|
.btn-sm{padding:6px 12px;font-size:11px}
|
|
.btn:hover{filter:brightness(1.1)}
|
|
.table{width:100%;border-collapse:collapse}
|
|
.table th,.table td{padding:10px;text-align:left;border-bottom:1px solid var(--border);font-size:12px}
|
|
.table th{background:var(--card2);color:var(--text2);text-transform:uppercase;font-size:11px}
|
|
.badge{padding:3px 8px;border-radius:12px;font-size:10px;font-weight:600}
|
|
.badge-success{background:rgba(16,185,129,0.2);color:var(--success)}
|
|
.badge-danger{background:rgba(239,68,68,0.2);color:var(--danger)}
|
|
.results-box{background:var(--card2);border-radius:8px;padding:12px;max-height:300px;overflow-y:auto;font-family:monospace;font-size:11px}
|
|
.results-box .line{padding:3px 0}
|
|
.results-box .success{color:var(--success)}
|
|
.results-box .error{color:var(--danger)}
|
|
.toast{position:fixed;bottom:20px;right:20px;background:var(--card);border:1px solid var(--border);padding:12px 20px;border-radius:8px;display:none;font-size:13px}
|
|
.toast.show{display:flex;align-items:center;gap:10px}
|
|
.toast.success{border-left:4px solid var(--success)}
|
|
.toast.error{border-left:4px solid var(--danger)}
|
|
</style>
|
|
|
|
</head>
|
|
<body>
|
|
<div class="sidebar">
|
|
<div class="logo"><i class="fab fa-google"></i> GSuite Manager</div>
|
|
<div class="nav-item active" onclick="showPanel('dashboard')"><i class="fas fa-home"></i> Dashboard</div>
|
|
<div class="nav-item" onclick="showPanel('users')"><i class="fas fa-users"></i> Users</div>
|
|
<div class="nav-item" onclick="showPanel('bulk')"><i class="fas fa-user-plus"></i> Bulk Create</div>
|
|
<div class="nav-item" onclick="showPanel('domain')"><i class="fas fa-exchange-alt"></i> Domain Changer</div>
|
|
<div class="nav-item" onclick="showPanel('domains')"><i class="fas fa-globe"></i> Domains</div>
|
|
<div class="nav-item" onclick="showPanel('operations')"><i class="fas fa-history"></i> Operations</div>
|
|
</div>
|
|
<div class="main">
|
|
<div class="header"><h1 id="pageTitle">Dashboard</h1></div>
|
|
<div class="panel active" id="panel-dashboard">
|
|
<div class="stats-grid">
|
|
<div class="stat-card"><div class="stat-value" id="statDomains">-</div><div class="stat-label">Domains</div></div>
|
|
<div class="stat-card"><div class="stat-value" id="statUsers">-</div><div class="stat-label">Users</div></div>
|
|
<div class="stat-card"><div class="stat-value" id="statOps">-</div><div class="stat-label">Ops (24h)</div></div>
|
|
</div>
|
|
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
|
<button class="btn btn-google" onclick="showPanel('bulk')"><i class="fas fa-user-plus"></i> Bulk Create</button>
|
|
<button class="btn btn-warning" onclick="showPanel('domain')"><i class="fas fa-exchange-alt"></i> Change Domain</button>
|
|
<button class="btn btn-primary" onclick="exportUsers()"><i class="fas fa-download"></i> Export CSV</button>
|
|
</div>
|
|
</div>
|
|
<div class="panel" id="panel-users">
|
|
<div class="panel-header"><h2><i class="fas fa-users"></i> Users</h2>
|
|
<select id="userDomainFilter" onchange="loadUsers()" style="padding:6px;background:var(--card2);border:1px solid var(--border);border-radius:6px;color:var(--text)"><option value="">All Domains</option></select>
|
|
</div>
|
|
<div class="panel-body" style="padding:0"><table class="table"><thead><tr><th>Email</th><th>Name</th><th>Password</th><th>Phone</th><th>Status</th><th>Actions</th></tr></thead><tbody id="usersTable"></tbody></table>
|
|
<div id="usersPagination" style="padding:16px;text-align:center"></div></div>
|
|
</div>
|
|
<div class="panel" id="panel-bulk">
|
|
<div class="panel-header"><h2><i class="fas fa-user-plus"></i> Bulk Create Users</h2></div>
|
|
<div class="panel-body">
|
|
<div class="form-row">
|
|
<div class="form-group"><label>Target Domain *</label><input type="text" id="bcDomain" placeholder="example.com"></div>
|
|
<div class="form-group"><label>Number of Users</label><input type="number" id="bcCount" value="20" min="1" max="500"></div>
|
|
</div>
|
|
<div class="form-group"><label>Names (FirstName;LastName per line)</label><textarea id="bcNames" placeholder="John;Doe Jane;Smith"></textarea></div>
|
|
<div class="form-group"><label>Upload CSV</label><input type="file" id="bcFile" accept=".csv,.txt" onchange="loadFile()"></div>
|
|
<div class="form-row">
|
|
<div class="form-group"><label>Email Format</label><select id="bcFormat"><option value="{first}{last}{rand}">firstlastXXXX</option><option value="{first}.{last}{rand}">first.lastXXXX</option><option value="{f}{last}{rand}">flastXXXX</option></select></div>
|
|
<div class="form-group"><label>Password</label><select id="bcPwdType" onchange="togglePwd()"><option value="generate">Auto Generate</option><option value="fixed">Fixed</option></select></div>
|
|
</div>
|
|
<div class="form-group" id="bcPwdGroup" style="display:none"><label>Fixed Password</label><input type="text" id="bcPwd" placeholder="Password123"></div>
|
|
<div class="form-row">
|
|
<div class="form-group"><label>Recovery Email</label><input type="email" id="bcRecEmail" placeholder="recovery@gmail.com"></div>
|
|
<div class="form-group"><label>Recovery Phones (comma separated)</label><input type="text" id="bcPhones" placeholder="+212666949338,+212666949339"></div>
|
|
</div>
|
|
<div class="form-group"><label>Phone Rotation (every N users)</label><input type="number" id="bcRotation" value="5" min="1"></div>
|
|
<button class="btn btn-success" onclick="bulkCreate()" id="bcBtn"><i class="fas fa-rocket"></i> Create Users</button>
|
|
<div id="bcResults" style="margin-top:20px;display:none"><h3 style="margin-bottom:10px;font-size:14px">Results</h3><div class="results-box" id="bcResultsBox"></div></div>
|
|
</div>
|
|
</div>
|
|
<div class="panel" id="panel-domain">
|
|
<div class="panel-header"><h2><i class="fas fa-exchange-alt"></i> Domain Changer</h2></div>
|
|
<div class="panel-body">
|
|
<div class="form-row">
|
|
<div class="form-group"><label>Source Domain *</label><input type="text" id="dcSource" placeholder="old-domain.com"></div>
|
|
<div class="form-group"><label>Target Domain *</label><input type="text" id="dcTarget" placeholder="new-domain.com"></div>
|
|
</div>
|
|
<div class="form-group"><label>Max Users</label><input type="number" id="dcLimit" value="100" min="1" max="500"></div>
|
|
<button class="btn btn-warning" onclick="changeDomain()" id="dcBtn"><i class="fas fa-exchange-alt"></i> Change Domain</button>
|
|
<div id="dcResults" style="margin-top:20px;display:none"><h3 style="margin-bottom:10px;font-size:14px">Results</h3><div class="results-box" id="dcResultsBox"></div></div>
|
|
</div>
|
|
</div>
|
|
<div class="panel" id="panel-domains">
|
|
<div class="panel-header"><h2><i class="fas fa-globe"></i> Domains</h2><button class="btn btn-sm btn-primary" onclick="loadDomains()"><i class="fas fa-sync"></i></button></div>
|
|
<div class="panel-body">
|
|
<div style="display:flex;gap:10px;margin-bottom:16px"><input type="text" id="newDomain" placeholder="Add domain..." style="flex:1;padding:10px;background:var(--card2);border:1px solid var(--border);border-radius:8px;color:var(--text)"><button class="btn btn-success" onclick="addDomain()"><i class="fas fa-plus"></i></button></div>
|
|
<table class="table"><thead><tr><th>Domain</th><th>Verified</th><th>Added</th></tr></thead><tbody id="domainsTable"></tbody></table>
|
|
</div>
|
|
</div>
|
|
<div class="panel" id="panel-operations">
|
|
<div class="panel-header"><h2><i class="fas fa-history"></i> Operations</h2></div>
|
|
<div class="panel-body" style="padding:0"><table class="table"><thead><tr><th>ID</th><th>Type</th><th>Status</th><th>Progress</th><th>Success</th><th>Errors</th><th>Date</th></tr></thead><tbody id="opsTable"></tbody></table></div>
|
|
</div>
|
|
</div>
|
|
<div class="toast" id="toast"><span id="toastText"></span></div>
|
|
<script>
|
|
let namesData=[];
|
|
function showPanel(p){document.querySelectorAll('.panel').forEach(x=>x.classList.remove('active'));document.querySelectorAll('.nav-item').forEach(x=>x.classList.remove('active'));document.getElementById('panel-'+p).classList.add('active');document.querySelector('.nav-item[onclick*="'+p+'"]').classList.add('active');document.getElementById('pageTitle').textContent=p.charAt(0).toUpperCase()+p.slice(1);if(p==='users')loadUsers();if(p==='domains')loadDomains();if(p==='operations')loadOps();}
|
|
async function loadStats(){const d=await fetch('?action=stats').then(r=>r.json());document.getElementById('statDomains').textContent=d.domains;document.getElementById('statUsers').textContent=d.users;document.getElementById('statOps').textContent=d.operations_24h;}
|
|
async function loadUsers(page=1){const domain=document.getElementById('userDomainFilter').value;const d=await fetch('?action=local_users&page='+page+'&domain='+domain).then(r=>r.json());document.getElementById('usersTable').innerHTML=d.users.map(u=>'<tr><td>'+u.email+'</td><td>'+u.first_name+' '+u.last_name+'</td><td><code style="background:var(--card2);padding:2px 6px;border-radius:4px">'+(u.password||'***')+'</code></td><td>'+(u.recovery_phone||'-')+'</td><td><span class="badge badge-'+(u.status==='active'?'success':'danger')+'">'+u.status+'</span></td><td><button class="btn btn-sm btn-danger" onclick="delUser(\''+u.email+'\')"><i class="fas fa-trash"></i></button></td></tr>').join('')||'<tr><td colspan="6" style="text-align:center;color:var(--text2)">No users</td></tr>';document.getElementById('usersPagination').innerHTML='<button class="btn btn-sm" onclick="loadUsers('+(page-1)+')" '+(page<=1?'disabled':'')+'>Prev</button> '+page+'/'+Math.max(1,d.pages)+' <button class="btn btn-sm" onclick="loadUsers('+(page+1)+')" '+(page>=d.pages?'disabled':'')+'>Next</button>';}
|
|
async function loadDomains(){const d=await fetch('?action=list_domains').then(r=>r.json());document.getElementById('domainsTable').innerHTML=d.map(x=>'<tr><td>'+x.domain+'</td><td>'+(x.verified?'<span class="badge badge-success">Yes</span>':'<span class="badge badge-danger">No</span>')+'</td><td>'+new Date(x.created_at).toLocaleDateString()+'</td></tr>').join('')||'<tr><td colspan="3" style="text-align:center;color:var(--text2)">No domains</td></tr>';const f=document.getElementById('userDomainFilter');f.innerHTML='<option value="">All</option>'+d.map(x=>'<option value="'+x.domain+'">'+x.domain+'</option>').join('');}
|
|
async function addDomain(){const d=document.getElementById('newDomain').value;if(!d)return;await fetch('?action=add_domain',{method:'POST',body:new URLSearchParams({domain:d})});document.getElementById('newDomain').value='';toast('Domain added','success');loadDomains();}
|
|
async function loadOps(){const d=await fetch('?action=operations').then(r=>r.json());document.getElementById('opsTable').innerHTML=d.map(o=>'<tr><td>#'+o.id+'</td><td>'+o.operation_type+'</td><td><span class="badge badge-'+(o.status==='completed'?'success':'danger')+'">'+o.status+'</span></td><td>'+o.processed_items+'/'+o.total_items+'</td><td style="color:var(--success)">'+o.success_count+'</td><td style="color:var(--danger)">'+o.error_count+'</td><td>'+new Date(o.created_at).toLocaleString()+'</td></tr>').join('')||'<tr><td colspan="7" style="text-align:center;color:var(--text2)">No operations</td></tr>';}
|
|
function loadFile(){const file=document.getElementById('bcFile').files[0];if(!file)return;const reader=new FileReader();reader.onload=function(e){const lines=e.target.result.split('\n').filter(l=>l.trim());namesData=[];for(let i=1;i<lines.length;i++){const p=lines[i].split(/[;,\t]/);if(p.length>=2)namesData.push({first:p[0].trim(),last:p[1].trim()});}document.getElementById('bcNames').value=namesData.map(n=>n.first+';'+n.last).join('\n');toast('Loaded '+namesData.length+' names','success');};reader.readAsText(file);}
|
|
function togglePwd(){document.getElementById('bcPwdGroup').style.display=document.getElementById('bcPwdType').value==='fixed'?'block':'none';}
|
|
async function bulkCreate(){const domain=document.getElementById('bcDomain').value;if(!domain){toast('Domain required','error');return;}if(!namesData.length){const lines=document.getElementById('bcNames').value.split('\n').filter(l=>l.trim());namesData=lines.map(l=>{const p=l.split(/[;,\t]/);return{first:p[0]?.trim()||'',last:p[1]?.trim()||''};}).filter(n=>n.first&&n.last);}if(!namesData.length){toast('Names required','error');return;}const btn=document.getElementById('bcBtn');btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Creating...';const fd=new FormData();fd.append('domain',domain);fd.append('names',JSON.stringify(namesData));fd.append('count',document.getElementById('bcCount').value);fd.append('email_format',document.getElementById('bcFormat').value);fd.append('password_type',document.getElementById('bcPwdType').value);fd.append('fixed_password',document.getElementById('bcPwd').value);fd.append('recovery_email',document.getElementById('bcRecEmail').value);fd.append('recovery_phones',document.getElementById('bcPhones').value);fd.append('phone_rotation',document.getElementById('bcRotation').value);const r=await fetch('?action=bulk_create',{method:'POST',body:fd}).then(r=>r.json());btn.disabled=false;btn.innerHTML='<i class="fas fa-rocket"></i> Create Users';document.getElementById('bcResults').style.display='block';document.getElementById('bcResultsBox').innerHTML=r.results.map(x=>'<div class="line '+(x.success?'success':'error')+'">'+(x.success?'OK':'ERR')+' '+x.email+' | '+x.password+' | '+(x.recovery_phone||'-')+'</div>').join('');toast('Created '+r.results.filter(x=>x.success).length+'/'+r.results.length,'success');loadStats();namesData=[];}
|
|
async function changeDomain(){const s=document.getElementById('dcSource').value,t=document.getElementById('dcTarget').value;if(!s||!t){toast('Both domains required','error');return;}const btn=document.getElementById('dcBtn');btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Processing...';const fd=new FormData();fd.append('source_domain',s);fd.append('target_domain',t);fd.append('limit',document.getElementById('dcLimit').value);const r=await fetch('?action=change_domain',{method:'POST',body:fd}).then(r=>r.json());btn.disabled=false;btn.innerHTML='<i class="fas fa-exchange-alt"></i> Change Domain';document.getElementById('dcResults').style.display='block';document.getElementById('dcResultsBox').innerHTML=r.results.map(x=>'<div class="line '+(x.success?'success':'error')+'">'+x.old_email+' -> '+x.new_email+'</div>').join('');toast('Changed '+r.results.filter(x=>x.success).length+'/'+r.results.length,'success');}
|
|
async function delUser(email){if(!confirm('Delete '+email+'?'))return;await fetch('?action=delete_user',{method:'POST',body:new URLSearchParams({email})});toast('Deleted','success');loadUsers();}
|
|
function exportUsers(){window.open('?action=export_users','_blank');}
|
|
function toast(msg,type='success'){const t=document.getElementById('toast');t.className='toast show '+type;document.getElementById('toastText').textContent=msg;setTimeout(()=>t.classList.remove('show'),3000);}
|
|
loadStats();loadDomains();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|