setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $db->exec('PRAGMA journal_mode=WAL'); $db->exec(" CREATE TABLE IF NOT EXISTS wearers ( access_uuid TEXT PRIMARY KEY, wearer_name TEXT DEFAULT '', region_name TEXT DEFAULT '', region_rating TEXT DEFAULT 'Unknown', diaper_state TEXT DEFAULT 'dry', mess_state TEXT DEFAULT 'Clean', mess_capable INTEGER DEFAULT 0, removal_locked TEXT DEFAULT '0', change_lock TEXT DEFAULT '0', auto_wet_interval INTEGER DEFAULT 1800, auto_mess_interval INTEGER DEFAULT 0, simulator_url TEXT DEFAULT '', last_seen INTEGER DEFAULT 0, owner_list TEXT DEFAULT '', announce_full TEXT DEFAULT '0', announce_leak TEXT DEFAULT '1', announce_change TEXT DEFAULT '1', notify_wearer TEXT DEFAULT '1', owner_title TEXT DEFAULT 'Caregiver', state_names TEXT DEFAULT 'Dry,Damp,Wet,Soaking,Leaking', mess_names TEXT DEFAULT 'Clean,Soiled,Messy,Blowout' ); CREATE TABLE IF NOT EXISTS owners ( owner_uuid TEXT PRIMARY KEY, password_hash TEXT NOT NULL DEFAULT '', password_changed INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS owner_wearers ( owner_uuid TEXT, access_uuid TEXT, PRIMARY KEY (owner_uuid, access_uuid) ); CREATE TABLE IF NOT EXISTS nicknames ( owner_uuid TEXT, target_uuid TEXT, nickname TEXT DEFAULT '', PRIMARY KEY (owner_uuid, target_uuid) ); "); return $db; } // ============================================================ // Wearer helpers // ============================================================ function getWearer($db, $uuid) { $stmt = $db->prepare('SELECT * FROM wearers WHERE access_uuid = ?'); $stmt->execute([$uuid]); return $stmt->fetch() ?: null; } function upsertWearer($db, $data) { $stmt = $db->prepare(" INSERT INTO wearers (access_uuid, wearer_name, region_name, region_rating, diaper_state, mess_state, mess_capable, removal_locked, change_lock, auto_wet_interval, auto_mess_interval, simulator_url, last_seen, owner_list, announce_full, announce_leak, announce_change, notify_wearer, owner_title, state_names, mess_names) VALUES (:access_uuid, :wearer_name, :region_name, :region_rating, :diaper_state, :mess_state, :mess_capable, :removal_locked, :change_lock, :auto_wet_interval, :auto_mess_interval, :simulator_url, :last_seen, :owner_list, :announce_full, :announce_leak, :announce_change, :notify_wearer, :owner_title, :state_names, :mess_names) ON CONFLICT(access_uuid) DO UPDATE SET wearer_name = excluded.wearer_name, region_name = excluded.region_name, region_rating = excluded.region_rating, diaper_state = excluded.diaper_state, mess_state = excluded.mess_state, mess_capable = excluded.mess_capable, removal_locked = excluded.removal_locked, change_lock = excluded.change_lock, auto_wet_interval = excluded.auto_wet_interval, auto_mess_interval = excluded.auto_mess_interval, simulator_url = excluded.simulator_url, last_seen = excluded.last_seen, owner_list = excluded.owner_list, announce_full = excluded.announce_full, announce_leak = excluded.announce_leak, announce_change = excluded.announce_change, notify_wearer = excluded.notify_wearer, owner_title = excluded.owner_title, state_names = excluded.state_names, mess_names = excluded.mess_names "); $stmt->execute([ ':access_uuid' => $data['access_uuid'] ?? '', ':wearer_name' => $data['wearer_name'] ?? '', ':region_name' => $data['region_name'] ?? '', ':region_rating' => $data['region_rating'] ?? 'Unknown', ':diaper_state' => $data['diaper_state'] ?? 'dry', ':mess_state' => $data['mess_state'] ?? 'Clean', ':mess_capable' => (int)($data['mess_capable'] ?? 0), ':removal_locked' => $data['removal_locked'] ?? '0', ':change_lock' => $data['change_lock'] ?? '0', ':auto_wet_interval' => (int)($data['auto_wet_interval'] ?? 1800), ':auto_mess_interval' => (int)($data['auto_mess_interval'] ?? 0), ':simulator_url' => $data['simulator_url'] ?? '', ':last_seen' => (int)($data['last_seen'] ?? time()), ':owner_list' => $data['owner_list'] ?? '', ':announce_full' => $data['announce_full'] ?? '0', ':announce_leak' => $data['announce_leak'] ?? '1', ':announce_change' => $data['announce_change'] ?? '1', ':notify_wearer' => $data['notify_wearer'] ?? '1', ':owner_title' => $data['owner_title'] ?? 'Caregiver', ':state_names' => $data['state_names'] ?? 'Dry,Damp,Wet,Soaking,Leaking', ':mess_names' => $data['mess_names'] ?? 'Clean,Soiled,Messy,Blowout', ]); } // Returns all wearers linked to an owner UUID function getWearersForOwner($db, $ownerUUID) { $stmt = $db->prepare(" SELECT w.* FROM wearers w JOIN owner_wearers ow ON ow.access_uuid = w.access_uuid WHERE ow.owner_uuid = ? ORDER BY w.wearer_name ASC "); $stmt->execute([$ownerUUID]); return $stmt->fetchAll(); } // ============================================================ // Owner helpers // ============================================================ function getOwner($db, $ownerUUID) { $stmt = $db->prepare('SELECT * FROM owners WHERE owner_uuid = ?'); $stmt->execute([$ownerUUID]); return $stmt->fetch() ?: null; } function ownerExists($db, $ownerUUID) { return getOwner($db, $ownerUUID) !== null; } function insertOwner($db, $ownerUUID, $passwordHash) { $stmt = $db->prepare( 'INSERT OR IGNORE INTO owners (owner_uuid, password_hash) VALUES (?, ?)' ); $stmt->execute([$ownerUUID, $passwordHash]); } function updateOwnerPassword($db, $ownerUUID, $passwordHash) { $stmt = $db->prepare( 'UPDATE owners SET password_hash = ?, password_changed = 0 WHERE owner_uuid = ?' ); $stmt->execute([$passwordHash, $ownerUUID]); } function linkOwnerWearer($db, $ownerUUID, $accessUUID) { $stmt = $db->prepare( 'INSERT OR IGNORE INTO owner_wearers (owner_uuid, access_uuid) VALUES (?, ?)' ); $stmt->execute([$ownerUUID, $accessUUID]); } // ============================================================ // Nickname helpers // ============================================================ function getNickname($db, $ownerUUID, $targetUUID) { $stmt = $db->prepare( 'SELECT nickname FROM nicknames WHERE owner_uuid = ? AND target_uuid = ?' ); $stmt->execute([$ownerUUID, $targetUUID]); $row = $stmt->fetch(); return $row ? $row['nickname'] : ''; } function setNickname($db, $ownerUUID, $targetUUID, $nickname) { $stmt = $db->prepare(" INSERT INTO nicknames (owner_uuid, target_uuid, nickname) VALUES (?, ?, ?) ON CONFLICT(owner_uuid, target_uuid) DO UPDATE SET nickname = excluded.nickname "); $stmt->execute([$ownerUUID, $targetUUID, $nickname]); } function getAllNicknames($db, $ownerUUID) { $stmt = $db->prepare( 'SELECT target_uuid, nickname FROM nicknames WHERE owner_uuid = ?' ); $stmt->execute([$ownerUUID]); $result = []; foreach ($stmt->fetchAll() as $row) { $result[$row['target_uuid']] = $row['nickname']; } return $result; } // ============================================================ // Cookie helpers // ============================================================ function setSessionCookie($name, $value, $days) { $opts = [ 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict', ]; if ($days > 0) $opts['expires'] = time() + (86400 * $days); setcookie($name, $value, $opts); } function clearCookie($name) { setcookie($name, '', [ 'expires' => time() - 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict', ]); } // ============================================================ // Display helpers // ============================================================ function getAgeString($lastSeen) { $age = time() - (int)$lastSeen; if ($age < 60) return 'Just now'; elseif ($age < 3600) return floor($age/60) . ' minutes ago'; elseif ($age < 86400) return floor($age/3600) . ' hours ago'; else return floor($age/86400) . ' days ago'; } function getChangeLockText($value) { switch ((string)$value) { case '0': return 'Open Access'; case '1': return 'Owner + Wearer'; case '2': return 'Owner Only'; default: return 'Unknown'; } } function isValidUUID($uuid) { return (bool)preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $uuid ); } // ============================================================ // Open database // ============================================================ $db = getDB($dbFile); // ============================================================ // POST - Receive data from HUD (JSON heartbeat / status update) // ============================================================ if ($_SERVER['REQUEST_METHOD'] === 'POST' && strpos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') !== false) { $input = file_get_contents('php://input'); $data = json_decode($input, true); if (!$data) { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Invalid JSON']); exit; } $action = $data['action'] ?? ''; // ---- proxy command to HUD ---- if (isset($data['proxy_cmd'])) { $ownerUUID = $_COOKIE[$cookieOwner] ?? ''; $wearerUUID = $_COOKIE[$cookieWearer] ?? ''; if (!$ownerUUID || !$wearerUUID) { http_response_code(403); echo json_encode(['status' => 'error', 'message' => 'Not logged in']); exit; } $stmt = $db->prepare( 'SELECT w.simulator_url FROM wearers w JOIN owner_wearers ow ON ow.access_uuid = w.access_uuid WHERE ow.owner_uuid = ? AND w.access_uuid = ?' ); $stmt->execute([$ownerUUID, $wearerUUID]); $row = $stmt->fetch(); if (!$row || empty($row['simulator_url'])) { http_response_code(503); echo json_encode(['status' => 'error', 'message' => 'HUD simulator URL not available. Is the wearer online?']); exit; } $forward = $data['proxy_cmd']; $forward['access_uuid'] = $wearerUUID; $ch = curl_init($row['simulator_url']); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($forward)); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($result === false) { http_response_code(503); echo json_encode(['status' => 'error', 'message' => 'Could not reach HUD. Wearer may have changed region or gone offline.']); exit; } http_response_code($httpCode); echo $result; exit; } // ---- add_owner ---- if ($action === 'add_owner') { $ownerUUID = trim($data['owner_uuid'] ?? ''); $plainPwd = trim($data['password'] ?? ''); $accessUUID = trim($data['access_uuid'] ?? ''); if (!isValidUUID($ownerUUID) || !isValidUUID($accessUUID) || $plainPwd === '') { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Missing or invalid fields']); exit; } $isNew = !ownerExists($db, $ownerUUID); if ($isNew) { insertOwner($db, $ownerUUID, password_hash($plainPwd, PASSWORD_DEFAULT)); } linkOwnerWearer($db, $ownerUUID, $accessUUID); echo json_encode(['status' => $isNew ? 'new' : 'existing']); exit; } // ---- reset_password ---- if ($action === 'reset_password') { $ownerUUID = trim($data['owner_uuid'] ?? ''); $plainPwd = trim($data['password'] ?? ''); if (!isValidUUID($ownerUUID) || $plainPwd === '') { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Missing or invalid fields']); exit; } updateOwnerPassword($db, $ownerUUID, password_hash($plainPwd, PASSWORD_DEFAULT)); echo json_encode(['status' => 'ok']); exit; } // ---- change_password (owner-initiated, authenticated) ---- if ($action === 'change_password') { $ownerUUID = $_COOKIE[$cookieOwner] ?? ''; $currentPwd = trim($data['current_password'] ?? ''); $newPwd = trim($data['new_password'] ?? ''); if (!$ownerUUID || !isValidUUID($ownerUUID) || $currentPwd === '' || $newPwd === '') { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Missing fields']); exit; } $owner = getOwner($db, $ownerUUID); if (!$owner || !password_verify($currentPwd, $owner['password_hash'])) { http_response_code(403); echo json_encode(['status' => 'error', 'message' => 'Current password incorrect']); exit; } $stmt = $db->prepare( 'UPDATE owners SET password_hash = ?, password_changed = 1 WHERE owner_uuid = ?' ); $stmt->execute([password_hash($newPwd, PASSWORD_DEFAULT), $ownerUUID]); echo json_encode(['status' => 'ok']); exit; } // ---- force_set_password (first-login forced change) ---- if ($action === 'force_set_password') { $ownerUUID = $_COOKIE[$cookieOwner] ?? ''; $newPwd = trim($data['new_password'] ?? ''); if (!$ownerUUID || !isValidUUID($ownerUUID) || $newPwd === '') { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Missing fields or not logged in']); exit; } $owner = getOwner($db, $ownerUUID); if (!$owner) { http_response_code(403); echo json_encode(['status' => 'error', 'message' => 'Owner not found']); exit; } $stmt = $db->prepare( 'UPDATE owners SET password_hash = ?, password_changed = 1 WHERE owner_uuid = ?' ); $stmt->execute([password_hash($newPwd, PASSWORD_DEFAULT), $ownerUUID]); echo json_encode(['status' => 'ok']); exit; } // ---- heartbeat / status update (no action field) ---- if (!isset($data['access_uuid'])) { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'No access_uuid']); exit; } upsertWearer($db, $data); echo json_encode(['status' => 'ok', 'message' => 'Data received']); exit; } // ============================================================ // POST - Owner login form // ============================================================ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['owner_uuid'])) { $ownerUUID = trim($_POST['owner_uuid']); $password = $_POST['password'] ?? ''; $remember = isset($_POST['remember']); if (!isValidUUID($ownerUUID)) { $loginError = 'Please enter a valid UUID.'; } else { $owner = getOwner($db, $ownerUUID); if (!$owner) { $loginError = 'Owner not found. Have you been added as an owner by a wearer?'; } elseif (!password_verify($password, $owner['password_hash'])) { $loginError = 'Incorrect password.'; } else { setSessionCookie($cookieOwner, $ownerUUID, $remember ? $cookieDays : 0); clearCookie($cookieWearer); // If password hasn't been changed yet, redirect to force-change page if (!$owner['password_changed']) { header('Location: ' . $_SERVER['PHP_SELF'] . '?mustchange=1'); } else { header('Location: ' . $_SERVER['PHP_SELF']); } exit; } } } // ============================================================ // POST - Select active wearer from dashboard // ============================================================ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['connect_wearer'])) { $ownerUUID = $_COOKIE[$cookieOwner] ?? ''; $accessUUID = trim($_POST['connect_wearer']); // Verify this owner actually has access to this wearer if ($ownerUUID && isValidUUID($accessUUID)) { $stmt = $db->prepare( 'SELECT 1 FROM owner_wearers WHERE owner_uuid = ? AND access_uuid = ?' ); $stmt->execute([$ownerUUID, $accessUUID]); if ($stmt->fetch()) { setSessionCookie($cookieWearer, $accessUUID, 0); } } header('Location: ' . $_SERVER['PHP_SELF']); exit; } // ============================================================ // POST - Remove wearer from owner's list (website only) // ============================================================ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_wearer'])) { $ownerUUID = $_COOKIE[$cookieOwner] ?? ''; $accessUUID = trim($_POST['remove_wearer']); if ($ownerUUID && isValidUUID($accessUUID)) { $stmt = $db->prepare( 'DELETE FROM owner_wearers WHERE owner_uuid = ? AND access_uuid = ?' ); $stmt->execute([$ownerUUID, $accessUUID]); } header('Location: ' . $_SERVER['PHP_SELF']); exit; } // ============================================================ // POST - Back to dashboard (deselect active wearer) // ============================================================ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['back_to_dashboard'])) { clearCookie($cookieWearer); header('Location: ' . $_SERVER['PHP_SELF']); exit; } // ============================================================ // POST - Save nickname // ============================================================ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'save_nickname') { $ownerUUID = $_COOKIE[$cookieOwner] ?? ''; $targetUUID = trim($_POST['owner_uuid'] ?? ''); $nickname = trim($_POST['nickname'] ?? ''); if ($ownerUUID && $targetUUID) { setNickname($db, $ownerUUID, $targetUUID, $nickname); } header('Location: ' . $_SERVER['PHP_SELF']); exit; } // ============================================================ // POST - Logout // ============================================================ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['logout'])) { clearCookie($cookieOwner); clearCookie($cookieWearer); header('Location: ' . $_SERVER['PHP_SELF']); exit; } // ============================================================ // Resolve session state // $ownerUUID — logged-in owner, or '' // $activeWearer — wearer row currently being viewed, or null // $wearers — all wearers for this owner (for dashboard) // ============================================================ $ownerUUID = $_COOKIE[$cookieOwner] ?? ''; $wearerUUID = $_COOKIE[$cookieWearer] ?? ''; $activeWearer = null; $wearers = []; $ownerRow = null; if ($ownerUUID) { $ownerRow = getOwner($db, $ownerUUID); if (!$ownerRow) { // Stale cookie — owner deleted clearCookie($cookieOwner); clearCookie($cookieWearer); $ownerUUID = ''; } else { $wearers = getWearersForOwner($db, $ownerUUID); if ($wearerUUID) { // Verify this owner still has access $stmt = $db->prepare( 'SELECT 1 FROM owner_wearers WHERE owner_uuid = ? AND access_uuid = ?' ); $stmt->execute([$ownerUUID, $wearerUUID]); if ($stmt->fetch()) { $activeWearer = getWearer($db, $wearerUUID); } else { clearCookie($cookieWearer); $wearerUUID = ''; } } // Load nicknames keyed by owner UUID (owner nicknames their wearers) $nicknames = getAllNicknames($db, $ownerUUID); } } // Determine which view to show // 0 = login 1 = dashboard (wearer list) 2 = control panel 3 = password change $view = 0; if ($ownerUUID) { $view = 1; // Force password change if flagged if ($ownerRow && !$ownerRow['password_changed']) $view = 3; } if ($activeWearer && $view !== 3) $view = 2; ?> HappyBums Remote Control Panel

HappyBums

Remote Control Panel
Your OpenSim UUID
Password
Your Wearers
You have no wearers linked to your account yet.
Ask your wearer to add you as an owner from their HUD.
600; ?>
 ·   · 
600; $ageStr = getAgeString($lastSeen); $state = strtolower($activeWearer['diaper_state'] ?? 'unknown'); $messState = $activeWearer['mess_state'] ?? 'Clean'; $messCapable = (int)($activeWearer['mess_capable'] ?? 0); $hudLocked = ($activeWearer['removal_locked'] ?? '0') == '1'; $changeLock = $activeWearer['change_lock'] ?? '0'; $ratingRaw = $activeWearer['region_rating'] ?? 0; $ratingLabels = ['General', 'Moderate', 'Adult']; $rating = $ratingLabels[(int)$ratingRaw] ?? 'Unknown'; $regionName = $activeWearer['region_name'] ?? 'Unknown'; $autoWet = (int)($activeWearer['auto_wet_interval'] ?? 1800); $autoWetMins = $autoWet > 0 ? (int)($autoWet / 60) : 0; $autoMess = (int)($activeWearer['auto_mess_interval'] ?? 0); $autoMessMins = $autoMess > 0 ? (int)($autoMess / 60) : 0; $ownerListRaw = $activeWearer['owner_list'] ?? ''; $owners = $ownerListRaw !== '' ? explode(',', $ownerListRaw) : []; // Nicknames for this owner (used in right column for listing owners) $nicknames = getAllNicknames($db, $ownerUUID); // Display name for the wearer (owner may have set a nickname) $wearerDisplayName = $nicknames[$wearerUUID] ?? $activeWearer['wearer_name']; $simUrl = $activeWearer['simulator_url'] ?? ''; ?>
Current Status
Wearer
Last Seen
⚠ HUD not responding — wearer may be offline or visiting another grid
Region
()

Diaper Status
Mess Status
HUD Lock
Change Lock
Owner Management
No owners currently assigned.
Send Message

Password Change Required

Your password was set by the in-world system. Please choose a new password before continuing.

New Password
Confirm New Password