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; ?>
Your password was set by the in-world system. Please choose a new password before continuing.