Files
WP-Ops/dev-panel.vincentdevelopment.ca/htdocs/index.php
2026-01-01 19:20:06 +00:00

838 lines
26 KiB
PHP

<?php
// Simple WordOps Panel - vanilla PHP with auth + roles + SQLite
// ---------- DB + Auth Helpers ----------
require_once __DIR__ . '/includes/functions.php';
session_start();
// Disable PHP output buffering for this script as much as we can
while (ob_get_level() > 0) {
ob_end_flush();
}
ob_implicit_flush(true);
// Send headers early
header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // Hint for Nginx: disable buffering
// Basic config
$woPath = '/usr/local/bin/wo';
$bootstrapScript = '/usr/local/bin/wp-dev-bootstrap.sh';
// Defaults for bootstrap script
$bootstrapAdminUser = 'vdidev';
$bootstrapAdminEmail = 'dev@vincentdevelopment.ca';
// Optional theme config for bootstrap script
$themeStarterRepo = 'git@github.com:Vincent-Design-Inc/VDI-Starter-v5.git';
$themeRemoteOrigin = ''; // e.g. 'git@github.com:your-org/client-theme-repo.git'
// SQLite config
$dbPath = __DIR__ . '/panel.sqlite';
// ---------- Init DB ----------
$seedInfo = initDb();
// ---------- Routing / Auth Gate ----------
$action = isset($_GET['action']) ? $_GET['action'] : 'list';
// Logout handler
if ($action === 'logout') {
$_SESSION = [];
session_destroy();
header('Location: ?action=login');
exit;
}
requireLogin($action);
$user = getCurrentUser();
// ---------- Login Action ----------
if ($action === 'login') {
$loginError = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if ($username !== '' && $password !== '') {
$db = getDb();
$stmt = $db->prepare('SELECT id, username, password_hash, role FROM users WHERE username = ?');
$stmt->execute([$username]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row && password_verify($password, $row['password_hash'])) {
$_SESSION['user_id'] = $row['id'];
$_SESSION['username'] = $row['username'];
$_SESSION['role'] = $row['role'];
header('Location: ?action=list');
exit;
} else {
$loginError = 'Invalid username or password.';
}
} else {
$loginError = 'Username and password are required.';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WordOps Dev Panel - Login</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<div>
<h1 style="margin:0;font-size:1.4rem;">WordOps Dev Panel</h1>
<p class="muted" style="margin-top:0.25rem;">Sign in to manage dev sites.</p>
</div>
</header>
<div class="card" style="max-width:420px;margin:2rem auto;">
<h2 style="margin-top:0;">Login</h2>
<?php
if (!empty($seedInfo['adminCreated'])) {
echo '<p class="muted"><strong>Initial admin created.</strong><br>Username: <code>admin</code><br>Password: <code>change-me</code>. Please log in and change it later.</p>';
}
if ($loginError) {
echo '<p><strong>' . htmlspecialchars($loginError) . '</strong></p>';
}
?>
<form method="post">
<label>
Username
<input type="text" name="username" required>
</label>
<label>
Password
<input type="password" name="password" required>
</label>
<div style="margin-top:1rem;">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
</div>
</body>
</html>
<?php
exit;
}
// ---------- Self password change (processed for any authenticated action) ----------
$selfPasswordError = null;
$selfPasswordMessage = null;
$passwordDialogOpen = false;
if (
$user &&
$_SERVER['REQUEST_METHOD'] === 'POST' &&
isset($_POST['op']) &&
$_POST['op'] === 'self_change_password'
) {
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$newPasswordConfirm = $_POST['new_password_confirm'] ?? '';
$passwordDialogOpen = true; // ensure dialog opens after post
if ($currentPassword === '' || $newPassword === '' || $newPasswordConfirm === '') {
$selfPasswordError = 'All password fields are required.';
} elseif ($newPassword !== $newPasswordConfirm) {
$selfPasswordError = 'New passwords do not match.';
} elseif (strlen($newPassword) < 8) {
$selfPasswordError = 'New password must be at least 8 characters.';
} else {
$db = getDb();
$stmt = $db->prepare('SELECT password_hash FROM users WHERE id = ?');
$stmt->execute([$user['id']]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row || !password_verify($currentPassword, $row['password_hash'])) {
$selfPasswordError = 'Current password is incorrect.';
} else {
$hash = password_hash($newPassword, PASSWORD_DEFAULT);
$update = $db->prepare('UPDATE users SET password_hash = ? WHERE id = ?');
$update->execute([$hash, $user['id']]);
$selfPasswordMessage = 'Your password has been updated.';
}
}
}
// ---------- Page Layout (for authenticated users) ----------
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WordOps Dev Panel</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<div>
<h1 style="margin:0;font-size:1.4rem;">WordOps Dev Panel</h1>
<p class="muted" style="margin-top:0.25rem;">Create and manage dev sites.</p>
</div>
<nav style="display:flex;align-items:center;gap:0.75rem;">
<a href="?action=list" class="btn">Sites</a>
<a href="?action=create" class="btn btn-primary">New site</a>
<?php if (isAdmin()): ?>
<a href="?action=users" class="btn">Users</a>
<?php endif; ?>
<div class="user-menu" style="position:relative;margin-left:auto;">
<button id="userMenuToggle" class="btn" type="button">
<?php echo htmlspecialchars($user['username']); ?>
<span class="muted" style="font-size:0.8rem;">(<?php echo htmlspecialchars($user['role']); ?>)</span>
</button>
<div id="userMenu" style="display:none;position:absolute;right:0;margin-top:0.25rem;min-width:180px;">
<div class="card" style="padding:0.5rem;">
<a href="#" id="changePasswordLink" class="muted" style="display:block;padding:0.25rem 0;">Change password</a>
<a href="?action=logout" class="muted danger" style="display:block;padding:0.25rem 0;">Logout</a>
</div>
</div>
</div>
</nav>
</header>
<?php
// ---------- Actions for authenticated users ----------
if ($action === 'create') {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (!isset($_POST['op']) || $_POST['op'] !== 'self_change_password')) {
$rawDomain = $_POST['domain'] ?? '';
$domain = sanitizeDomain($rawDomain);
$projectName = trim($_POST['project_name'] ?? '');
$stackType = $_POST['stack_type'] ?? 'wp';
$isMultisite = isset($_POST['multisite']) ? $_POST['multisite'] : 'no';
$bootstrapProfile = $_POST['bootstrap_profile'] ?? 'none';
if (!$projectName) {
$projectName = $domain;
}
if (!$domain) {
echo '<div class="card"><strong>Invalid domain.</strong> Use only letters, numbers, dots and dashes.</div>';
} else {
// Build WordOps command
$flags = [];
if ($stackType === 'wp') {
$flags[] = '--wp';
} elseif ($stackType === 'wpfc') {
$flags[] = '--wpfc';
} elseif ($stackType === 'plain') {
$flags[] = '--php';
}
if ($isMultisite === 'subdir') {
$flags[] = '--wpsubdir';
} elseif ($isMultisite === 'subdomain') {
$flags[] = '--wpsubdomain';
}
$cmd = sprintf(
'sudo %s site create %s %s',
escapeshellcmd($woPath),
escapeshellarg($domain),
implode(' ', array_map('escapeshellarg', $flags))
);
echo '<div class="card">';
echo '<h2 style="margin-top:0;">Provisioning site: ' . htmlspecialchars($domain) . '</h2>';
echo '<p class="muted">WordOps log:</p>';
echo '<pre>';
@ob_flush();
@flush();
list($ok, $woLines) = runCommandStreaming($cmd, function ($line) {
echo htmlspecialchars($line) . "\n";
@ob_flush();
@flush();
});
echo '</pre>';
if ($ok) {
// Record site owner in SQLite
$db = getDb();
$stmt = $db->prepare('INSERT OR REPLACE INTO sites (domain, owner_id, created_at) VALUES (?, ?, datetime("now"))');
$stmt->execute([$domain, $user['id']]);
echo '<p><strong>Site created successfully.</strong></p>';
// Run bootstrap if requested
if ($bootstrapProfile === 'standard') {
$adminUser = $bootstrapAdminUser;
$adminEmail = $bootstrapAdminEmail;
$bootstrapArgs = [
'--domain', $domain,
'--project-name', $projectName,
'--admin-user', $adminUser,
'--admin-email', $adminEmail,
];
if ($themeStarterRepo !== '') {
$bootstrapArgs[] = '--theme-starter-repo';
$bootstrapArgs[] = $themeStarterRepo;
}
if ($themeRemoteOrigin !== '') {
$bootstrapArgs[] = '--theme-remote-origin';
$bootstrapArgs[] = $themeRemoteOrigin;
}
$bootstrapCmd = escapeshellcmd($bootstrapScript);
foreach ($bootstrapArgs as $arg) {
$bootstrapCmd .= ' ' . escapeshellarg($arg);
}
echo '<p class="muted" style="margin-top:1rem;">Bootstrap log:</p>';
echo '<pre>';
@ob_flush();
@flush();
list($bootstrapOk, $bootstrapLines) = runCommandStreaming($bootstrapCmd, function ($line) {
echo htmlspecialchars($line) . "\n";
@ob_flush();
@flush();
});
echo '</pre>';
if ($bootstrapOk) {
echo '<p><strong>Bootstrap completed:</strong> standard dev stack applied.</p>';
} else {
echo '<p><strong>Bootstrap failed:</strong> see log above.</p>';
}
}
echo '<p><a class="btn" href="?action=list">Back to site list</a></p>';
} else {
echo '<p><strong>Site creation failed.</strong> See WordOps log above.</p>';
echo '<p><a class="btn" href="?action=list">Back to site list</a></p>';
}
echo '</div>';
}
}
?>
<div class="card">
<h2 style="margin-top:0;">Create new site</h2>
<form method="post">
<label>
Domain
<input type="text" name="domain" placeholder="client-project.dev.internal" required>
<span class="muted">Use the same naming convention you plan for the real server.</span>
</label>
<label>
Project name
<input type="text" name="project_name" placeholder="Client Project Name">
<span class="muted">Used for site title and theme naming. Defaults to the domain if left blank.</span>
</label>
<label>
Stack type
<select name="stack_type">
<option value="wp">WordPress (no cache)</option>
<option value="wpfc">WordPress + fastcgi cache</option>
<option value="plain">Plain PHP (no WP, for tools)</option>
</select>
</label>
<label>
Multisite (optional)
<select name="multisite">
<option value="no">No (single site)</option>
<option value="subdir">Multisite (subdirectory)</option>
<option value="subdomain">Multisite (subdomain)</option>
</select>
</label>
<label>
Bootstrap profile
<select name="bootstrap_profile">
<option value="none">None (bare site only)</option>
<option value="standard">Standard dev stack (plugins, pages, theme)</option>
</select>
<span class="muted">“Standard” runs the shared bootstrap script after site creation.</span>
</label>
<div style="margin-top:1rem;">
<button type="submit" class="btn btn-primary">Create site</button>
<a href="?action=list" class="btn">Cancel</a>
</div>
</form>
</div>
<?php
} elseif ($action === 'delete' && isset($_GET['domain'])) {
$domain = sanitizeDomain($_GET['domain']);
if (!$domain) {
echo '<div class="card"><strong>Invalid domain.</strong></div>';
} elseif (
$_SERVER['REQUEST_METHOD'] === 'POST' &&
isset($_POST['confirm']) &&
$_POST['confirm'] === 'yes' &&
(!isset($_POST['op']) || $_POST['op'] !== 'self_change_password')
) {
$confirmText = $_POST['confirm_text'] ?? '';
if ($confirmText !== $domain) {
echo '<div class="card"><strong>Confirmation text did not match domain.</strong></div>';
} else {
// Enforce ownership for dev users
$db = getDb();
$ownerRow = $db->prepare('SELECT owner_id FROM sites WHERE domain = ?');
$ownerRow->execute([$domain]);
$ownerId = $ownerRow->fetchColumn();
if (!isAdmin() && (int)$ownerId !== (int)$user['id']) {
echo '<div class="card"><strong>You do not have permission to delete this site.</strong></div>';
} else {
$cmd = sprintf(
'sudo %s site delete %s --no-prompt',
escapeshellcmd($woPath),
escapeshellarg($domain)
);
$output = [];
$status = null;
$ok = runCommand($cmd, $output, $status);
echo '<div class="card">';
if ($ok) {
// Remove from metadata
$stmt = $db->prepare('DELETE FROM sites WHERE domain = ?');
$stmt->execute([$domain]);
echo '<strong>Site deleted:</strong> ' . htmlspecialchars($domain) . '<br>';
} else {
echo '<strong>Failed to delete site.</strong> Check the log below.';
}
echo '<pre>' . htmlspecialchars(implode("\n", $output)) . '</pre>';
echo '<p><a class="btn" href="?action=list">Back to site list</a></p>';
echo '</div>';
}
}
} else {
?>
<div class="card">
<h2 style="margin-top:0;">Delete site</h2>
<p>You're about to delete <strong><?php echo htmlspecialchars($domain); ?></strong>.</p>
<p class="muted">This will remove the vhost and files. Databases are handled by WordOps according to its defaults.</p>
<form method="post">
<label>
Type the domain to confirm
<input type="text" name="confirm_text" placeholder="<?php echo htmlspecialchars($domain); ?>" required>
</label>
<input type="hidden" name="confirm" value="yes">
<div style="margin-top:1rem;">
<button type="submit" class="btn btn-danger">Yes, delete this site</button>
<a href="?action=list" class="btn">Cancel</a>
</div>
</form>
</div>
<?php
}
} elseif ($action === 'users' && isAdmin()) {
$db = getDb();
$userMessage = null;
$userError = null;
$generatedPasswordInfo = null;
if (
$_SERVER['REQUEST_METHOD'] === 'POST' &&
isset($_POST['op']) &&
$_POST['op'] !== 'self_change_password'
) {
$op = $_POST['op'] ?? '';
if ($op === 'create') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$passwordConfirm = $_POST['password_confirm'] ?? '';
$role = $_POST['role'] ?? 'dev';
if ($username === '' || $password === '' || $passwordConfirm === '') {
$userError = 'Username and both password fields are required.';
} elseif ($password !== $passwordConfirm) {
$userError = 'Passwords do not match.';
} elseif (!in_array($role, ['admin', 'dev'], true)) {
$userError = 'Invalid role selected.';
} else {
// Check uniqueness
$stmt = $db->prepare('SELECT COUNT(*) FROM users WHERE username = ?');
$stmt->execute([$username]);
if ((int)$stmt->fetchColumn() > 0) {
$userError = 'Username already exists.';
} else {
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)');
$stmt->execute([$username, $hash, $role]);
$userMessage = 'User "' . htmlspecialchars($username) . '" created successfully.';
}
}
} elseif ($op === 'reset_password') {
$userId = (int)($_POST['user_id'] ?? 0);
if ($userId > 0) {
$stmt = $db->prepare('SELECT username FROM users WHERE id = ?');
$stmt->execute([$userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
$userError = 'User not found.';
} else {
$newPass = generatePassword(16);
$hash = password_hash($newPass, PASSWORD_DEFAULT);
$update = $db->prepare('UPDATE users SET password_hash = ? WHERE id = ?');
$update->execute([$hash, $userId]);
$generatedPasswordInfo = [
'username' => $row['username'],
'password' => $newPass,
];
$userMessage = 'Password reset for user "' . htmlspecialchars($row['username']) . '".';
}
} else {
$userError = 'Invalid user ID.';
}
}
}
// Fetch users for listing
$stmt = $db->query('SELECT id, username, role FROM users ORDER BY username ASC');
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="card">
<h2 style="margin-top:0;">User Management</h2>
<?php
if ($userMessage) {
echo '<p><strong>' . $userMessage . '</strong></p>';
}
if ($userError) {
echo '<p><strong>' . htmlspecialchars($userError) . '</strong></p>';
}
if ($generatedPasswordInfo) {
echo '<p class="muted">New password for <strong>' . htmlspecialchars($generatedPasswordInfo['username']) . '</strong>: ';
echo '<code>' . htmlspecialchars($generatedPasswordInfo['password']) . '</code></p>';
}
?>
<h3>Existing users</h3>
<?php if (empty($users)): ?>
<p class="muted">No users found.</p>
<?php else: ?>
<table style="width:100%;border-collapse:collapse;margin-bottom:1rem;">
<thead>
<tr>
<th style="text-align:left;border-bottom:1px solid #1f2937;padding:0.4rem 0.2rem;">Username</th>
<th style="text-align:left;border-bottom:1px solid #1f2937;padding:0.4rem 0.2rem;">Role</th>
<th style="text-align:left;border-bottom:1px solid #1f2937;padding:0.4rem 0.2rem;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $u): ?>
<tr>
<td style="padding:0.3rem 0.2rem;"><?php echo htmlspecialchars($u['username']); ?></td>
<td style="padding:0.3rem 0.2rem;"><?php echo htmlspecialchars($u['role']); ?></td>
<td style="padding:0.3rem 0.2rem;">
<form method="post" style="display:inline;">
<input type="hidden" name="op" value="reset_password">
<input type="hidden" name="user_id" value="<?php echo (int)$u['id']; ?>">
<button type="submit" class="btn btn-danger" style="font-size:0.75rem;padding:0.2rem 0.6rem;">
Reset password
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<h3>Create new user</h3>
<form method="post">
<input type="hidden" name="op" value="create">
<label>
Username
<input type="text" name="username" required>
</label>
<label>
Role
<select name="role">
<option value="dev">Developer</option>
<option value="admin">Admin</option>
</select>
</label>
<label>
Password
<input type="password" name="password" required>
</label>
<label>
Confirm password
<input type="password" name="password_confirm" required>
</label>
<div style="margin-top:1rem;">
<button type="submit" class="btn btn-primary">Create user</button>
</div>
</form>
</div>
<?php
} else {
// Default: site list
$cmd = sprintf('sudo %s site list', escapeshellcmd($woPath));
$output = [];
$status = null;
$ok = runCommand($cmd, $output, $status);
echo '<div class="card">';
echo '<h2 style="margin-top:0;">Sites</h2>';
if (!$ok) {
echo '<strong>Could not retrieve site list.</strong>';
echo '<pre>' . htmlspecialchars(implode("\n", $output)) . '</pre>';
} else {
$db = getDb();
// Map of domain => [owner_id, username, role]
$meta = [];
$stmt = $db->query('
SELECT s.domain, s.owner_id, u.username, u.role
FROM sites s
LEFT JOIN users u ON s.owner_id = u.id
');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$meta[$row['domain']] = $row;
}
$sites = [];
foreach ($output as $line) {
$line = trim($line);
// Skip header / separator lines and the panel site itself
if (
$line === '' ||
strpos($line, 'Site') !== false ||
strpos($line, 'site') !== false ||
strpos($line, '---') !== false ||
$line === 'dev-panel.local'
) {
continue;
}
// Take the first column as the domain
$parts = preg_split('/\s+/', $line);
if (!$parts || !isset($parts[0])) {
continue;
}
$domain = sanitizeDomain($parts[0]);
if (!$domain) {
continue;
}
$ownerInfo = $meta[$domain] ?? null;
// Role-based filtering: dev sees only their own; admin sees all
if (!isAdmin()) {
if (!$ownerInfo || (int)$ownerInfo['owner_id'] !== (int)$user['id']) {
continue;
}
}
$sites[] = [
'domain' => $domain,
'owner' => $ownerInfo['username'] ?? null,
'ownerRole' => $ownerInfo['role'] ?? null,
'ownerId' => $ownerInfo['owner_id'] ?? null,
];
}
if (empty($sites) || (count($sites) === 1 && strPos($sites[0]['domain'], 'dev-panel') !== false)) {
echo '<p class="muted">No sites found for your account yet. <a href="?action=create">Create your first site</a>.</p>';
} else {
echo '<ul style="list-style:none;padding-left:0;">';
foreach ($sites as $siteInfo) {
if (strPos($siteInfo['domain'], 'dev-panel') !== false) {
continue;
}
$site = $siteInfo['domain'];
$owner = $siteInfo['owner'];
$siteEsc = htmlspecialchars($site);
$url = 'http://' . $site;
$adminUrl = 'http://' . $site . '/wp-admin/';
echo '<li style="margin-bottom:0.75rem;">';
echo '<strong>' . $siteEsc . '</strong>';
echo ' <span class="tag">dev</span>';
if ($owner) {
echo ' <span class="muted" style="margin-left:0.5rem;">Owner: ' . htmlspecialchars($owner) . '</span>';
} else {
echo ' <span class="muted" style="margin-left:0.5rem;">Owner: Unassigned</span>';
}
echo '<br>';
echo '<a href="' . $url . '" target="_blank" rel="noopener">Open site</a> · ';
echo '<a href="' . $adminUrl . '" target="_blank" rel="noopener">WP Admin</a> · ';
echo '<a href="?action=delete&amp;domain=' . urlencode($site) . '" class="muted danger">Delete…</a>';
echo '</li>';
}
echo '</ul>';
}
}
echo '</div>';
}
?>
<!-- Change Password Modal -->
<div id="passwordModal" style="display:none;position:fixed;inset:0;z-index:50;align-items:center;justify-content:center;background:rgba(0,0,0,0.7);">
<div class="card" style="max-width:420px;width:90%;margin:0 auto;">
<h2 style="margin-top:0;">Change password</h2>
<?php if ($selfPasswordError): ?>
<p><strong><?php echo htmlspecialchars($selfPasswordError); ?></strong></p>
<?php endif; ?>
<?php if ($selfPasswordMessage): ?>
<p><strong><?php echo htmlspecialchars($selfPasswordMessage); ?></strong></p>
<?php endif; ?>
<form method="post">
<input type="hidden" name="op" value="self_change_password">
<label>
Current password
<input type="password" name="current_password" required>
</label>
<label>
New password
<input type="password" name="new_password" required>
</label>
<label>
Confirm new password
<input type="password" name="new_password_confirm" required>
</label>
<div style="margin-top:1rem;display:flex;gap:0.5rem;justify-content:flex-end;">
<button type="button" class="btn" id="passwordModalClose">Cancel</button>
<button type="submit" class="btn btn-primary">Update password</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const userMenuToggle = document.getElementById('userMenuToggle');
const userMenu = document.getElementById('userMenu');
const changePasswordLink = document.getElementById('changePasswordLink');
const passwordModal = document.getElementById('passwordModal');
const passwordModalClose = document.getElementById('passwordModalClose');
if (userMenuToggle && userMenu) {
userMenuToggle.addEventListener('click', function (e) {
e.preventDefault();
userMenu.style.display = (userMenu.style.display === 'block') ? 'none' : 'block';
});
document.addEventListener('click', function (e) {
if (!userMenu.contains(e.target) && e.target !== userMenuToggle) {
userMenu.style.display = 'none';
}
});
}
function openPasswordModal() {
if (passwordModal) {
passwordModal.style.display = 'flex';
}
}
function closePasswordModal() {
if (passwordModal) {
passwordModal.style.display = 'none';
}
}
if (changePasswordLink) {
changePasswordLink.addEventListener('click', function (e) {
e.preventDefault();
openPasswordModal();
});
}
if (passwordModalClose) {
passwordModalClose.addEventListener('click', function (e) {
e.preventDefault();
closePasswordModal();
});
}
if (passwordModal) {
passwordModal.addEventListener('click', function (e) {
if (e.target === passwordModal) {
closePasswordModal();
}
});
}
<?php if ($passwordDialogOpen): ?>
// Re-open dialog after POST for feedback
openPasswordModal();
<?php endif; ?>
});
</script>
</body>
</html>