fix aws
This commit is contained in:
@@ -116,6 +116,21 @@ CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
INDEX idx_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Admin users table for RBAC
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
role ENUM('staff', 'admin') NOT NULL DEFAULT 'staff',
|
||||
display_name VARCHAR(255) DEFAULT NULL,
|
||||
active TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(255) DEFAULT NULL,
|
||||
UNIQUE KEY unique_email (email),
|
||||
INDEX idx_role (role),
|
||||
INDEX idx_active (active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- PTR records cache table for Route53 A records
|
||||
CREATE TABLE IF NOT EXISTS ptr_records_cache (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
|
||||
155
webapp/api.php
155
webapp/api.php
@@ -230,6 +230,23 @@ try {
|
||||
handleRoute53RecordsByPrefix($db);
|
||||
break;
|
||||
|
||||
// Admin user management (admin only)
|
||||
case 'admin_users_list':
|
||||
handleAdminUsersList($db);
|
||||
break;
|
||||
|
||||
case 'admin_user_save':
|
||||
handleAdminUserSave($db);
|
||||
break;
|
||||
|
||||
case 'admin_user_delete':
|
||||
handleAdminUserDelete($db);
|
||||
break;
|
||||
|
||||
case 'admin_user_toggle':
|
||||
handleAdminUserToggle($db);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => 'Invalid action'], 400);
|
||||
}
|
||||
@@ -3280,3 +3297,141 @@ function handleAwsUpdateARecord($db) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all admin users (admin only)
|
||||
*/
|
||||
function handleAdminUsersList($db) {
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
|
||||
if (!isAdmin()) {
|
||||
jsonResponse(['error' => 'Admin access required'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$users = getAdminUsers();
|
||||
jsonResponse(['success' => true, 'users' => $users]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save (create or update) an admin user (admin only)
|
||||
*/
|
||||
function handleAdminUserSave($db) {
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
|
||||
if (!isAdmin()) {
|
||||
jsonResponse(['error' => 'Admin access required'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['error' => 'POST method required'], 405);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['email'])) {
|
||||
jsonResponse(['error' => 'Email is required'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$email = $input['email'];
|
||||
$role = $input['role'] ?? ROLE_STAFF;
|
||||
$displayName = $input['display_name'] ?? null;
|
||||
$createdBy = getAuditUser();
|
||||
|
||||
$success = saveAdminUser($email, $role, $displayName, $createdBy);
|
||||
|
||||
if ($success) {
|
||||
jsonResponse(['success' => true, 'message' => 'User saved successfully']);
|
||||
} else {
|
||||
jsonResponse(['error' => 'Failed to save user'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an admin user (admin only)
|
||||
*/
|
||||
function handleAdminUserDelete($db) {
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
|
||||
if (!isAdmin()) {
|
||||
jsonResponse(['error' => 'Admin access required'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['error' => 'POST method required'], 405);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['id'])) {
|
||||
jsonResponse(['error' => 'User ID is required'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent deleting yourself
|
||||
$currentUser = getCurrentUser();
|
||||
$stmt = $db->prepare("SELECT email FROM admin_users WHERE id = ?");
|
||||
$stmt->execute([$input['id']]);
|
||||
$userToDelete = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($userToDelete && strtolower($userToDelete['email']) === strtolower($currentUser['email'])) {
|
||||
jsonResponse(['error' => 'You cannot delete your own account'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$success = deleteAdminUser($input['id']);
|
||||
|
||||
if ($success) {
|
||||
jsonResponse(['success' => true, 'message' => 'User deleted successfully']);
|
||||
} else {
|
||||
jsonResponse(['error' => 'Failed to delete user or user not found'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle an admin user's active status (admin only)
|
||||
*/
|
||||
function handleAdminUserToggle($db) {
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
|
||||
if (!isAdmin()) {
|
||||
jsonResponse(['error' => 'Admin access required'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['error' => 'POST method required'], 405);
|
||||
return;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['id'])) {
|
||||
jsonResponse(['error' => 'User ID is required'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent deactivating yourself
|
||||
$currentUser = getCurrentUser();
|
||||
$stmt = $db->prepare("SELECT email FROM admin_users WHERE id = ?");
|
||||
$stmt->execute([$input['id']]);
|
||||
$userToToggle = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($userToToggle && strtolower($userToToggle['email']) === strtolower($currentUser['email'])) {
|
||||
jsonResponse(['error' => 'You cannot deactivate your own account'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$success = toggleAdminUser($input['id']);
|
||||
|
||||
if ($success) {
|
||||
jsonResponse(['success' => true, 'message' => 'User status toggled successfully']);
|
||||
} else {
|
||||
jsonResponse(['error' => 'Failed to toggle user status'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Authentication and RBAC Helper Functions
|
||||
*
|
||||
* Provides role-based access control with Cloudflare Access integration
|
||||
* Admin users are stored in the database (admin_users table)
|
||||
*/
|
||||
|
||||
// Role constants
|
||||
@@ -19,24 +20,20 @@ function getCurrentUser() {
|
||||
// Check for Cloudflare Access headers first
|
||||
// Try multiple header variations that Cloudflare might use
|
||||
$cfEmail = null;
|
||||
$headerChecked = [];
|
||||
|
||||
// Standard Cloudflare Access header
|
||||
if (!empty($_SERVER['HTTP_CF_ACCESS_AUTHENTICATED_USER_EMAIL'])) {
|
||||
$cfEmail = $_SERVER['HTTP_CF_ACCESS_AUTHENTICATED_USER_EMAIL'];
|
||||
$headerChecked[] = 'HTTP_CF_ACCESS_AUTHENTICATED_USER_EMAIL';
|
||||
}
|
||||
// Alternative: some proxies might pass it differently
|
||||
elseif (!empty($_SERVER['CF_ACCESS_AUTHENTICATED_USER_EMAIL'])) {
|
||||
$cfEmail = $_SERVER['CF_ACCESS_AUTHENTICATED_USER_EMAIL'];
|
||||
$headerChecked[] = 'CF_ACCESS_AUTHENTICATED_USER_EMAIL';
|
||||
}
|
||||
// Check via getallheaders() for non-standard server configs
|
||||
elseif (function_exists('getallheaders')) {
|
||||
$headers = getallheaders();
|
||||
if (!empty($headers['Cf-Access-Authenticated-User-Email'])) {
|
||||
$cfEmail = $headers['Cf-Access-Authenticated-User-Email'];
|
||||
$headerChecked[] = 'getallheaders:Cf-Access-Authenticated-User-Email';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +63,67 @@ function getCurrentUser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user role from database or default to staff
|
||||
* Ensure admin_users table exists and seed from ADMIN_EMAILS if empty
|
||||
*/
|
||||
function ensureAdminUsersTable() {
|
||||
if (!function_exists('getDB')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
|
||||
// Create table if not exists
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
role ENUM('staff', 'admin') NOT NULL DEFAULT 'staff',
|
||||
display_name VARCHAR(255) DEFAULT NULL,
|
||||
active TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(255) DEFAULT NULL,
|
||||
UNIQUE KEY unique_email (email),
|
||||
INDEX idx_role (role),
|
||||
INDEX idx_active (active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
");
|
||||
|
||||
// Check if table is empty
|
||||
$count = $db->query("SELECT COUNT(*) FROM admin_users")->fetchColumn();
|
||||
|
||||
if ($count == 0) {
|
||||
// Seed from ADMIN_EMAILS environment variable or constant
|
||||
$adminEmails = getenv('ADMIN_EMAILS');
|
||||
if (empty($adminEmails) && defined('ADMIN_EMAILS')) {
|
||||
$adminEmails = ADMIN_EMAILS;
|
||||
}
|
||||
|
||||
if (!empty($adminEmails)) {
|
||||
$emails = array_map('trim', explode(',', $adminEmails));
|
||||
$stmt = $db->prepare("INSERT INTO admin_users (email, role, created_by) VALUES (?, 'admin', 'system_seed')");
|
||||
|
||||
foreach ($emails as $email) {
|
||||
if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
try {
|
||||
$stmt->execute([strtolower($email)]);
|
||||
} catch (Exception $e) {
|
||||
// Ignore duplicates
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user role from database
|
||||
*
|
||||
* @param string $email User email
|
||||
* @return string Role (admin or staff)
|
||||
@@ -79,45 +136,14 @@ function getUserRole($email) {
|
||||
// Normalize email for comparison (lowercase, trimmed)
|
||||
$email = strtolower(trim($email));
|
||||
|
||||
// Try multiple ways to get ADMIN_EMAILS (different PHP configs handle env vars differently)
|
||||
$adminEmails = '';
|
||||
// Ensure table exists and is seeded
|
||||
ensureAdminUsersTable();
|
||||
|
||||
// Method 1: getenv()
|
||||
if (empty($adminEmails)) {
|
||||
$adminEmails = getenv('ADMIN_EMAILS');
|
||||
}
|
||||
|
||||
// Method 2: $_ENV superglobal
|
||||
if (empty($adminEmails) && isset($_ENV['ADMIN_EMAILS'])) {
|
||||
$adminEmails = $_ENV['ADMIN_EMAILS'];
|
||||
}
|
||||
|
||||
// Method 3: $_SERVER (some configs put env vars here)
|
||||
if (empty($adminEmails) && isset($_SERVER['ADMIN_EMAILS'])) {
|
||||
$adminEmails = $_SERVER['ADMIN_EMAILS'];
|
||||
}
|
||||
|
||||
// Method 4: Defined constant from config.php
|
||||
if (empty($adminEmails) && defined('ADMIN_EMAILS')) {
|
||||
$adminEmails = ADMIN_EMAILS;
|
||||
}
|
||||
|
||||
// Check if email is in admin list
|
||||
if (!empty($adminEmails)) {
|
||||
$adminList = array_map(function($e) {
|
||||
return strtolower(trim($e));
|
||||
}, explode(',', $adminEmails));
|
||||
|
||||
if (in_array($email, $adminList)) {
|
||||
return ROLE_ADMIN;
|
||||
}
|
||||
}
|
||||
|
||||
// Try database lookup if getDB function exists
|
||||
// Try database lookup
|
||||
if (function_exists('getDB')) {
|
||||
try {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT role FROM users WHERE LOWER(email) = ? AND active = 1");
|
||||
$stmt = $db->prepare("SELECT role FROM admin_users WHERE LOWER(email) = ? AND active = 1");
|
||||
$stmt->execute([$email]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -129,7 +155,7 @@ function getUserRole($email) {
|
||||
}
|
||||
}
|
||||
|
||||
// Default to staff if not found in env or database
|
||||
// Default to staff if not found in database
|
||||
return ROLE_STAFF;
|
||||
}
|
||||
|
||||
@@ -238,8 +264,26 @@ function getUserDisplayInfo() {
|
||||
$name = explode('@', $email)[0];
|
||||
$initials = strtoupper(substr($name, 0, 2));
|
||||
|
||||
// Try to get display name from database
|
||||
$displayName = null;
|
||||
if (function_exists('getDB')) {
|
||||
try {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT display_name FROM admin_users WHERE LOWER(email) = ?");
|
||||
$stmt->execute([strtolower($email)]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($result && !empty($result['display_name'])) {
|
||||
$displayName = $result['display_name'];
|
||||
$nameParts = explode(' ', $displayName);
|
||||
$initials = strtoupper(substr($nameParts[0], 0, 1) . (isset($nameParts[1]) ? substr($nameParts[1], 0, 1) : substr($nameParts[0], 1, 1)));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => ucfirst($name),
|
||||
'name' => $displayName ?: ucfirst($name),
|
||||
'email' => $email,
|
||||
'role' => $user['role'],
|
||||
'initials' => $initials,
|
||||
@@ -247,3 +291,101 @@ function getUserDisplayInfo() {
|
||||
'is_admin' => $user['role'] === ROLE_ADMIN
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all admin users from database
|
||||
*
|
||||
* @return array List of admin users
|
||||
*/
|
||||
function getAdminUsers() {
|
||||
if (!function_exists('getDB')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
$stmt = $db->query("SELECT id, email, role, display_name, active, created_at, created_by FROM admin_users ORDER BY role DESC, email ASC");
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Exception $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update an admin user
|
||||
*
|
||||
* @param string $email User email
|
||||
* @param string $role User role (staff or admin)
|
||||
* @param string|null $displayName Display name
|
||||
* @param string $createdBy Who created this user
|
||||
* @return bool Success
|
||||
*/
|
||||
function saveAdminUser($email, $role, $displayName = null, $createdBy = null) {
|
||||
if (!function_exists('getDB')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$email = strtolower(trim($email));
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($role, [ROLE_STAFF, ROLE_ADMIN])) {
|
||||
$role = ROLE_STAFF;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO admin_users (email, role, display_name, created_by)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE role = VALUES(role), display_name = VALUES(display_name), updated_at = CURRENT_TIMESTAMP
|
||||
");
|
||||
$stmt->execute([$email, $role, $displayName, $createdBy]);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an admin user
|
||||
*
|
||||
* @param int $id User ID
|
||||
* @return bool Success
|
||||
*/
|
||||
function deleteAdminUser($id) {
|
||||
if (!function_exists('getDB')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("DELETE FROM admin_users WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
return $stmt->rowCount() > 0;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle admin user active status
|
||||
*
|
||||
* @param int $id User ID
|
||||
* @return bool Success
|
||||
*/
|
||||
function toggleAdminUser($id) {
|
||||
if (!function_exists('getDB')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("UPDATE admin_users SET active = NOT active, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
return $stmt->rowCount() > 0;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ $showSettingsLink = false;
|
||||
|
||||
// Get current tab from URL
|
||||
$currentTab = $_GET['tab'] ?? 'integrations';
|
||||
$validTabs = ['integrations', 'audit', 'advanced', 'whitelabel', 'developer'];
|
||||
$validTabs = ['integrations', 'users', 'audit', 'advanced', 'whitelabel', 'developer'];
|
||||
if (!in_array($currentTab, $validTabs)) {
|
||||
$currentTab = 'integrations';
|
||||
}
|
||||
@@ -71,6 +71,15 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</svg>
|
||||
Integrations
|
||||
</a>
|
||||
<a href="?tab=users" class="settings-nav-item <?php echo $currentTab === 'users' ? 'active' : ''; ?>">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
Users
|
||||
</a>
|
||||
<a href="?tab=audit" class="settings-nav-item <?php echo $currentTab === 'audit' ? 'active' : ''; ?>">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
@@ -283,6 +292,71 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users Section -->
|
||||
<div class="settings-section <?php echo $currentTab === 'users' ? 'active' : ''; ?>" id="section-users">
|
||||
<div class="advanced-section">
|
||||
<h2 class="advanced-section-title">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
User Management
|
||||
</h2>
|
||||
<p class="advanced-section-desc">Manage admin and staff users who can access this application. Users are authenticated via Cloudflare Access.</p>
|
||||
|
||||
<!-- Add User Form -->
|
||||
<div class="form-grid" style="margin-top: 16px; margin-bottom: 24px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email Address</label>
|
||||
<input type="email" class="form-input" id="newUserEmail" placeholder="user@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Display Name (optional)</label>
|
||||
<input type="text" class="form-input" id="newUserDisplayName" placeholder="John Doe">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 16px;">
|
||||
<label class="form-label">Role</label>
|
||||
<select class="form-select" id="newUserRole" style="max-width: 300px;">
|
||||
<option value="staff">Staff - Can view and edit Geofeed/PTR entries</option>
|
||||
<option value="admin">Admin - Full access including settings</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="addUser()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
Add User
|
||||
</button>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="table-container" style="margin-top: 24px;">
|
||||
<table class="data-table" id="usersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Display Name</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody">
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; padding: 40px;">
|
||||
<div class="loading"><div class="spinner"></div></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit Log Section -->
|
||||
<div class="settings-section <?php echo $currentTab === 'audit' ? 'active' : ''; ?>" id="section-audit">
|
||||
<div class="advanced-section">
|
||||
@@ -512,6 +586,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
loadWebhookSettings();
|
||||
loadWebhookQueueStatus();
|
||||
break;
|
||||
case 'users':
|
||||
loadUsers();
|
||||
break;
|
||||
case 'audit':
|
||||
loadAuditLog();
|
||||
break;
|
||||
@@ -528,6 +605,177 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// User Management Functions
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const response = await fetch('api.php?action=admin_users_list', {
|
||||
headers: { 'X-CSRF-Token': CSRF_TOKEN }
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to load users');
|
||||
}
|
||||
|
||||
const tbody = document.getElementById('usersTableBody');
|
||||
if (data.users.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--text-tertiary);">No users found. Add a user above.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.users.map(user => `
|
||||
<tr data-user-id="${user.id}">
|
||||
<td>${escapeHtml(user.email)}</td>
|
||||
<td>${user.display_name ? escapeHtml(user.display_name) : '<span style="color: var(--text-tertiary);">-</span>'}</td>
|
||||
<td>
|
||||
<span class="badge ${user.role === 'admin' ? 'badge-purple' : 'badge-blue'}">
|
||||
${user.role === 'admin' ? 'Admin' : 'Staff'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge ${user.active == 1 ? 'badge-success' : 'badge-warning'}">
|
||||
${user.active == 1 ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${formatDate(user.created_at)}</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="btn btn-secondary btn-sm" onclick="toggleUser(${user.id})" title="${user.active == 1 ? 'Deactivate' : 'Activate'}">
|
||||
${user.active == 1 ?
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>' :
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'
|
||||
}
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteUser(${user.id}, '${escapeHtml(user.email)}')" title="Delete">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error('Error loading users:', error);
|
||||
showNotification('Failed to load users: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function addUser() {
|
||||
const email = document.getElementById('newUserEmail').value.trim();
|
||||
const displayName = document.getElementById('newUserDisplayName').value.trim();
|
||||
const role = document.getElementById('newUserRole').value;
|
||||
|
||||
if (!email) {
|
||||
showNotification('Please enter an email address', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('api.php?action=admin_user_save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
display_name: displayName || null,
|
||||
role: role
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to add user');
|
||||
}
|
||||
|
||||
showNotification('User added successfully', 'success');
|
||||
document.getElementById('newUserEmail').value = '';
|
||||
document.getElementById('newUserDisplayName').value = '';
|
||||
document.getElementById('newUserRole').value = 'staff';
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Error adding user:', error);
|
||||
showNotification('Failed to add user: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleUser(userId) {
|
||||
try {
|
||||
const response = await fetch('api.php?action=admin_user_toggle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({ id: userId })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to toggle user status');
|
||||
}
|
||||
|
||||
showNotification('User status updated', 'success');
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Error toggling user:', error);
|
||||
showNotification('Failed to toggle user: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(userId, email) {
|
||||
if (!confirm(`Are you sure you want to delete the user "${email}"? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('api.php?action=admin_user_delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({ id: userId })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to delete user');
|
||||
}
|
||||
|
||||
showNotification('User deleted successfully', 'success');
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error);
|
||||
showNotification('Failed to delete user: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
|
||||
Reference in New Issue
Block a user