Compare commits

..

5 Commits

Author SHA1 Message Date
7937261925 🐞 fix: Update code formatting 2026-03-07 23:08:50 +00:00
7babef1372 🔵 other: Remove phpinfo file 2026-03-07 22:47:09 +00:00
d7ca1abece feature: Update bootstrap script 2026-03-07 22:35:04 +00:00
dev
e8d324e89d Samba shares, host file updates 2026-03-07 22:24:24 +00:00
dev
292e7e98bc Samba shares, host file updates 2026-03-07 22:23:55 +00:00
11 changed files with 707 additions and 560 deletions

View File

@@ -1,3 +1,5 @@
[user] [user]
name = VDI Devs name = VDI Devs
email = dev@vincentdesign.ca email = dev@vincentdesign.ca
[safe]
directory = /var/www

7
.gitignore vendored
View File

@@ -4,3 +4,10 @@
.well-known/ .well-known/
logs/ logs/
*.sqlite *.sqlite
.cache/
.config/
.local/
.npm/
.zcompdump
*.local

1
.zshrc Normal file
View File

@@ -0,0 +1 @@
# Created by newuser for 5.9

View File

@@ -10,6 +10,7 @@ session_start();
while (ob_get_level() > 0) { while (ob_get_level() > 0) {
ob_end_flush(); ob_end_flush();
} }
ob_implicit_flush(true); ob_implicit_flush(true);
// Send headers early // Send headers early
@@ -33,11 +34,9 @@ $themeRemoteOrigin = ''; // e.g. 'git@github.com:your-org/client-theme-repo.git'
$dbPath = __DIR__ . '/panel.sqlite'; $dbPath = __DIR__ . '/panel.sqlite';
// ---------- Init DB ---------- // ---------- Init DB ----------
$seedInfo = initDb(); $seedInfo = initDb();
// ---------- Routing / Auth Gate ---------- // ---------- Routing / Auth Gate ----------
$action = isset($_GET['action']) ? $_GET['action'] : 'list'; $action = isset($_GET['action']) ? $_GET['action'] : 'list';
// Logout handler // Logout handler
@@ -52,9 +51,9 @@ requireLogin($action);
$user = getCurrentUser(); $user = getCurrentUser();
// ---------- Login Action ---------- // ---------- Login Action ----------
if ($action === 'login') { if ($action === 'login') {
$loginError = null; $loginError = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? ''); $username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? ''; $password = $_POST['password'] ?? '';
@@ -79,8 +78,8 @@ if ($action === 'login') {
$loginError = 'Username and password are required.'; $loginError = 'Username and password are required.';
} }
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -133,7 +132,6 @@ if ($action === 'login') {
} }
// ---------- Self password change (processed for any authenticated action) ---------- // ---------- Self password change (processed for any authenticated action) ----------
$selfPasswordError = null; $selfPasswordError = null;
$selfPasswordMessage = null; $selfPasswordMessage = null;
$passwordDialogOpen = false; $passwordDialogOpen = false;
@@ -172,10 +170,9 @@ if (
} }
} }
} }
// ---------- Page Layout (for authenticated users) ---------- // ---------- Page Layout (for authenticated users) ----------
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -215,12 +212,9 @@ if (
</nav> </nav>
</header> </header>
<?php <?php
// ---------- Actions for authenticated users ----------
// ---------- Actions for authenticated users ---------- if ($action === 'create') {
if ($action === 'create') {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (!isset($_POST['op']) || $_POST['op'] !== 'self_change_password')) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && (!isset($_POST['op']) || $_POST['op'] !== 'self_change_password')) {
$rawDomain = $_POST['domain'] ?? ''; $rawDomain = $_POST['domain'] ?? '';
$domain = sanitizeDomain($rawDomain); $domain = sanitizeDomain($rawDomain);
@@ -300,12 +294,14 @@ if ($action === 'create') {
$bootstrapArgs[] = '--theme-starter-repo'; $bootstrapArgs[] = '--theme-starter-repo';
$bootstrapArgs[] = $themeStarterRepo; $bootstrapArgs[] = $themeStarterRepo;
} }
if ($themeRemoteOrigin !== '') { if ($themeRemoteOrigin !== '') {
$bootstrapArgs[] = '--theme-remote-origin'; $bootstrapArgs[] = '--theme-remote-origin';
$bootstrapArgs[] = $themeRemoteOrigin; $bootstrapArgs[] = $themeRemoteOrigin;
} }
$bootstrapCmd = escapeshellcmd($bootstrapScript); $bootstrapCmd = escapeshellcmd($bootstrapScript);
foreach ($bootstrapArgs as $arg) { foreach ($bootstrapArgs as $arg) {
$bootstrapCmd .= ' ' . escapeshellarg($arg); $bootstrapCmd .= ' ' . escapeshellarg($arg);
} }
@@ -340,7 +336,6 @@ if ($action === 'create') {
echo '</div>'; echo '</div>';
} }
} }
?> ?>
<div class="card"> <div class="card">
@@ -394,8 +389,7 @@ if ($action === 'create') {
</div> </div>
<?php <?php
} elseif ($action === 'delete' && isset($_GET['domain'])) {
} elseif ($action === 'delete' && isset($_GET['domain'])) {
$domain = sanitizeDomain($_GET['domain']); $domain = sanitizeDomain($_GET['domain']);
if (!$domain) { if (!$domain) {
@@ -470,9 +464,7 @@ if ($action === 'create') {
</div> </div>
<?php <?php
} }
} elseif ($action === 'users' && isAdmin()) {
} elseif ($action === 'users' && isAdmin()) {
$db = getDb(); $db = getDb();
$userMessage = null; $userMessage = null;
$userError = null; $userError = null;
@@ -501,6 +493,7 @@ if ($action === 'create') {
// Check uniqueness // Check uniqueness
$stmt = $db->prepare('SELECT COUNT(*) FROM users WHERE username = ?'); $stmt = $db->prepare('SELECT COUNT(*) FROM users WHERE username = ?');
$stmt->execute([$username]); $stmt->execute([$username]);
if ((int)$stmt->fetchColumn() > 0) { if ((int)$stmt->fetchColumn() > 0) {
$userError = 'Username already exists.'; $userError = 'Username already exists.';
} else { } else {
@@ -512,6 +505,7 @@ if ($action === 'create') {
} }
} elseif ($op === 'reset_password') { } elseif ($op === 'reset_password') {
$userId = (int)($_POST['user_id'] ?? 0); $userId = (int)($_POST['user_id'] ?? 0);
if ($userId > 0) { if ($userId > 0) {
$stmt = $db->prepare('SELECT username FROM users WHERE id = ?'); $stmt = $db->prepare('SELECT username FROM users WHERE id = ?');
$stmt->execute([$userId]); $stmt->execute([$userId]);
@@ -529,6 +523,7 @@ if ($action === 'create') {
'username' => $row['username'], 'username' => $row['username'],
'password' => $newPass, 'password' => $newPass,
]; ];
$userMessage = 'Password reset for user "' . htmlspecialchars($row['username']) . '".'; $userMessage = 'Password reset for user "' . htmlspecialchars($row['username']) . '".';
} }
} else { } else {
@@ -549,9 +544,11 @@ if ($action === 'create') {
if ($userMessage) { if ($userMessage) {
echo '<p><strong>' . $userMessage . '</strong></p>'; echo '<p><strong>' . $userMessage . '</strong></p>';
} }
if ($userError) { if ($userError) {
echo '<p><strong>' . htmlspecialchars($userError) . '</strong></p>'; echo '<p><strong>' . htmlspecialchars($userError) . '</strong></p>';
} }
if ($generatedPasswordInfo) { if ($generatedPasswordInfo) {
echo '<p class="muted">New password for <strong>' . htmlspecialchars($generatedPasswordInfo['username']) . '</strong>: '; echo '<p class="muted">New password for <strong>' . htmlspecialchars($generatedPasswordInfo['username']) . '</strong>: ';
echo '<code>' . htmlspecialchars($generatedPasswordInfo['password']) . '</code></p>'; echo '<code>' . htmlspecialchars($generatedPasswordInfo['password']) . '</code></p>';
@@ -623,8 +620,7 @@ if ($action === 'create') {
</form> </form>
</div> </div>
<?php <?php
} else {
} else {
// Default: site list // Default: site list
$cmd = sprintf('sudo %s site list', escapeshellcmd($woPath)); $cmd = sprintf('sudo %s site list', escapeshellcmd($woPath));
$output = []; $output = [];
@@ -642,16 +638,19 @@ if ($action === 'create') {
// Map of domain => [owner_id, username, role] // Map of domain => [owner_id, username, role]
$meta = []; $meta = [];
$stmt = $db->query(' $stmt = $db->query('
SELECT s.domain, s.owner_id, u.username, u.role SELECT s.domain, s.owner_id, u.username, u.role
FROM sites s FROM sites s
LEFT JOIN users u ON s.owner_id = u.id LEFT JOIN users u ON s.owner_id = u.id
'); ');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$meta[$row['domain']] = $row; $meta[$row['domain']] = $row;
} }
$sites = []; $sites = [];
foreach ($output as $line) { foreach ($output as $line) {
$line = trim($line); $line = trim($line);
@@ -668,11 +667,13 @@ if ($action === 'create') {
// Take the first column as the domain // Take the first column as the domain
$parts = preg_split('/\s+/', $line); $parts = preg_split('/\s+/', $line);
if (!$parts || !isset($parts[0])) { if (!$parts || !isset($parts[0])) {
continue; continue;
} }
$domain = sanitizeDomain($parts[0]); $domain = sanitizeDomain($parts[0]);
if (!$domain) { if (!$domain) {
continue; continue;
} }
@@ -712,28 +713,30 @@ if ($action === 'create') {
echo '<li style="margin-bottom:0.75rem;">'; echo '<li style="margin-bottom:0.75rem;">';
echo '<strong>' . $siteEsc . '</strong>'; echo '<strong>' . $siteEsc . '</strong>';
echo ' <span class="tag">dev</span>'; echo ' <span class="tag">dev</span>';
if ($owner) { if ($owner) {
echo ' <span class="muted" style="margin-left:0.5rem;">Owner: ' . htmlspecialchars($owner) . '</span>'; echo ' <span class="muted" style="margin-left:0.5rem;">Owner: ' . htmlspecialchars($owner) . '</span>';
} else { } else {
echo ' <span class="muted" style="margin-left:0.5rem;">Owner: Unassigned</span>'; echo ' <span class="muted" style="margin-left:0.5rem;">Owner: Unassigned</span>';
} }
echo '<br>'; echo '<br>';
echo '<a href="' . $url . '" target="_blank" rel="noopener">Open site</a> · '; 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="' . $adminUrl . '" target="_blank" rel="noopener">WP Admin</a> · ';
echo '<a href="?action=delete&amp;domain=' . urlencode($site) . '" class="muted danger">Delete…</a>'; echo '<a href="?action=delete&amp;domain=' . urlencode($site) . '" class="muted danger">Delete…</a>';
echo '</li>'; echo '</li>';
} }
echo '</ul>'; echo '</ul>';
} }
} }
echo '</div>'; 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);">
<!-- 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;"> <div class="card" style="max-width:420px;width:90%;margin:0 auto;">
<h2 style="margin-top:0;">Change password</h2> <h2 style="margin-top:0;">Change password</h2>
@@ -769,10 +772,10 @@ if ($action === 'create') {
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const userMenuToggle = document.getElementById('userMenuToggle'); const userMenuToggle = document.getElementById('userMenuToggle');
const userMenu = document.getElementById('userMenu'); const userMenu = document.getElementById('userMenu');
const changePasswordLink = document.getElementById('changePasswordLink'); const changePasswordLink = document.getElementById('changePasswordLink');
@@ -830,8 +833,7 @@ document.addEventListener('DOMContentLoaded', function () {
// Re-open dialog after POST for feedback // Re-open dialog after POST for feedback
openPasswordModal(); openPasswordModal();
<?php endif; ?> <?php endif; ?>
}); });
</script> </script>
</body> </body>
</html> </html>

8
helpers/dev-sites.path Normal file
View File

@@ -0,0 +1,8 @@
[Unit]
Description=Watch for new dev sites
[Path]
PathModified=/var/www
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,6 @@
[Unit]
Description=Update hosts entries for dev sites
[Service]
Type=oneshot
ExecStart=/usr/local/bin/update-dev-hosts.sh

56
helpers/gen-wpcontent-shares Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
wwwRoot="/var/www"
outFile="/etc/samba/wordops-wpcontent-shares.conf"
tmpFile="$(mktemp)"
# Change these to match your world:
sambaGroup="dev" # group allowed to access shares
forceGroup="dev" # group to force on created files (optional)
{
echo "; AUTO-GENERATED FILE - DO NOT EDIT"
echo "; Generated: $(date -Is)"
echo
shopt -s nullglob
for siteDir in "${wwwRoot}"/*; do
site="$(basename "${siteDir}")"
wpContent="${wwwRoot}/${site}/htdocs/wp-content"
# Only create shares for sites that look like WP installs
if [[ -d "${wpContent}" ]]; then
cat <<SHARE
[${site}]
comment = WordPress wp-content for ${site}
path = ${wpContent}
browseable = yes
writable = yes
read only = no
guest ok = no
; Lock access down to a group
valid users = @${sambaGroup}
; Keep permissions sane for webserver + devs
force group = ${forceGroup}
create mask = 0664
directory mask = 2775
; Optional: reduce Finder/Windows junk
veto files = /Thumbs.db/.DS_Store/._.DS_Store/
SHARE
fi
done
} > "${tmpFile}"
# Basic sanity check: refuse to install a broken file
testparm -s "${tmpFile}" >/dev/null
sudo mv "${tmpFile}" "${outFile}"
sudo chmod 0644 "${outFile}"
# Reload Samba to pick up new shares (no disconnect like restart)
sudo systemctl reload smbd || sudo systemctl reload samba

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Watch /var/www and regenerate Samba shares on change
[Path]
PathChanged=/var/www
PathModified=/var/www
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,6 @@
[Unit]
Description=Generate Samba shares for WordPress wp-content folders
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/gen-wpcontent-shares

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
HOSTS_FILE="/etc/hosts"
SITES_DIR="/var/www"
DEV_DOMAIN="vincentdevelopment.ca"
TMP=$(mktemp)
echo "# BEGIN DEV-SITES" >> "$TMP"
for dir in "$SITES_DIR"/*; do
site=$(basename "$dir")
# Skip system dirs
[[ "$site" == "html" ]] && continue
echo "127.0.0.1 $site" >> "$TMP"
# Uncomment when system goes live
# echo "127.0.0.1 $site $site.$DEV_DOMAIN"
done
echo "# END DEV-SITES" >> "$TMP"
# Remove existing block
sed -i '/# BEGIN DEV-SITES/,/# END DEV-SITES/d' "$HOSTS_FILE"
# Append fresh block
cat "$TMP" >> "$HOSTS_FILE"
rm "$TMP"

View File

@@ -90,8 +90,10 @@ else
if wp user list --field=user_email | grep -q "^${ADMIN_EMAIL}\$"; then if wp user list --field=user_email | grep -q "^${ADMIN_EMAIL}\$"; then
echo "Admin email ${ADMIN_EMAIL} already in use; skipping user create." echo "Admin email ${ADMIN_EMAIL} already in use; skipping user create."
else else
wp user create "$ADMIN_USER" "$ADMIN_EMAIL" --role=administrator --user_pass="$(openssl rand -base64 16)" PASS=$(openssl rand -base64 16)
echo "Admin user $ADMIN_USER created with email $ADMIN_EMAIL (random password)."
wp user create "$ADMIN_USER" "$ADMIN_EMAIL" --role=administrator --user_pass="$PASS"
echo "Admin user $ADMIN_USER created with email $ADMIN_EMAIL ($PASS)."
fi fi
fi fi
@@ -119,6 +121,12 @@ DEV_PLUGINS=(
query-monitor query-monitor
user-switching user-switching
debug-bar debug-bar
https://docs.vincentdevelopment.ca/files/advanced-custom-fields-pro.zip
https://docs.vincentdevelopment.ca/files/gravity-forms.zip
autodescription
better-search-replace
google-site-kit
simple-history
) )
for PLUGIN in "${DEV_PLUGINS[@]}"; do for PLUGIN in "${DEV_PLUGINS[@]}"; do
@@ -151,7 +159,7 @@ if [[ -n "$THEME_STARTER_REPO" ]]; then
mkdir -p "$THEMES_DIR" mkdir -p "$THEMES_DIR"
# Slug from project name # Slug from project name
THEME_SLUG=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-') THEME_SLUG=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/-$//')
[[ -z "$THEME_SLUG" ]] && THEME_SLUG="custom-theme" [[ -z "$THEME_SLUG" ]] && THEME_SLUG="custom-theme"
TARGET_THEME_DIR="${THEMES_DIR}/${THEME_SLUG}" TARGET_THEME_DIR="${THEMES_DIR}/${THEME_SLUG}"
@@ -187,4 +195,15 @@ else
echo "==> Theme starter repo not provided; skipping theme bootstrap." echo "==> Theme starter repo not provided; skipping theme bootstrap."
fi fi
echo "==> Installing dependencies..."
cd "$TARGET_THEME_DIR"
/usr/local/bin/composer install
/usr/bin/npm install
echo "==> Doing initial build..."
/usr/bin/npm run build
echo "==> Updating site permissions..."
sudo /usr/local/bin/wo-fix-perms.sh "$DOMAIN"
echo "==> Bootstrap complete." echo "==> Bootstrap complete."