member_srl ?? 0)); } function launcher_member_is_site_admin(int $memberSrl): bool { if ($memberSrl <= 0) { return false; } $autoload = dirname(__DIR__, 2) . '/common/autoload.php'; if (!is_file($autoload)) { return false; } try { require_once $autoload; if (!class_exists('Context') || !class_exists('MemberModel')) { return false; } Context::init(); $memberInfo = MemberModel::getMemberInfoByMemberSrl($memberSrl); return is_object($memberInfo) && (($memberInfo->is_site_admin ?? false) === true || ($memberInfo->is_admin ?? 'N') === 'Y'); } catch (Throwable $e) { return false; } } function launcher_issue_admin_ticket(string $audience, int $memberSrl): string { if ($memberSrl <= 0) { return ''; } return custom_signed_cookie_payload([ 'scope' => 'launcher_admin_ticket', 'aud' => $audience, 'admin' => true, 'member_srl' => $memberSrl, 'iat' => time(), 'exp' => time() + LAUNCHER_ADMIN_TICKET_TTL, ]); } function launcher_validate_admin_ticket(string $ticket, string $audience): ?array { if ($ticket === '' || strlen($ticket) > 4096 || $audience === '') { return null; } $payload = custom_verify_signed_cookie_payload($ticket); if (!is_array($payload)) { return null; } $memberSrl = (int)($payload['member_srl'] ?? 0); $valid = ($payload['scope'] ?? '') === 'launcher_admin_ticket' && ($payload['aud'] ?? '') === $audience && !empty($payload['admin']) && $memberSrl > 0 && (int)($payload['iat'] ?? 0) <= time() + 30 && (int)($payload['exp'] ?? 0) >= time() && launcher_member_is_site_admin($memberSrl); return $valid ? $payload : null; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { custom_block_login_denied(true); $data = custom_read_request_data(); $action = $data['action'] ?? 'login'; if ($action === 'login') { $password = (string)($data['password'] ?? ''); if (!launcher_is_digit_password($password)) { custom_json(['result' => 'fail', 'message' => 'password_digits_only'], 401); } if (custom_verify_password($password)) { custom_issue_auth_cookie(false); $pwaService = (string)($data['pwaService'] ?? $_GET['pwaService'] ?? ''); $pwaIssued = $pwaService !== '' && function_exists('custom_issue_pwa_auth_cookie') ? custom_issue_pwa_auth_cookie($pwaService) : false; custom_json(['result' => 'ok', 'pwaIssued' => $pwaIssued]); } custom_json(['result' => 'fail'], 401); } if ($action === 'logout') { $scope = (string)($data['scope'] ?? ''); if ($scope === 'admin') { custom_clear_auth_cookie(); } elseif (function_exists('custom_clear_non_admin_auth_cookie')) { custom_clear_non_admin_auth_cookie(false); } else { custom_clear_auth_cookie(); } custom_json(['result' => 'ok']); } if ($action === 'changePassword') { $current = (string)($data['currentPassword'] ?? ''); $next = (string)($data['newPassword'] ?? ''); if (!launcher_is_digit_password($current) || !launcher_is_digit_password($next)) { custom_json(['result' => 'fail', 'message' => 'password_digits_only'], 422); } if (!custom_verify_password($current)) { custom_json(['result' => 'fail', 'message' => 'current_password_invalid'], 401); } if (!custom_save_password_hash(password_hash($next, PASSWORD_DEFAULT))) { custom_json(['result' => 'fail', 'message' => 'password_save_failed'], 500); } custom_issue_auth_cookie(false); custom_json(['result' => 'ok']); } if ($action === 'poke') { $sender = trim((string)($data['sender'] ?? '')); $message = trim((string)($data['message'] ?? '')); $ok = custom_ha_webhook('poke', [ 'sender' => $sender !== '' ? $sender : '익명', 'message' => $message, ]); custom_json(['result' => $ok ? 'ok' : 'fail'], $ok ? 200 : 502); } custom_json(['error' => 'not_found'], 404); } if ($_SERVER['REQUEST_METHOD'] === 'GET') { if (isset($_GET['adminBridge'])) { $audience = launcher_admin_ticket_audience((string)($_GET['adminBridge'] ?? '')); $returnUrl = trim((string)($_GET['return'] ?? '')); $returnParts = parse_url($returnUrl); $validReturn = $audience !== '' && is_array($returnParts) && strtolower((string)($returnParts['scheme'] ?? '')) === 'https' && strtolower((string)($returnParts['host'] ?? '')) === 'eodiya.net' && !isset($returnParts['port']) && !isset($returnParts['user']) && !isset($returnParts['pass']) && !isset($returnParts['fragment']); if (!$validReturn) { custom_json(['error' => 'invalid_admin_bridge_return'], 400); } $memberSrl = launcher_current_site_admin_member_srl(); $isAdmin = $memberSrl > 0; if ($isAdmin) { custom_issue_auth_cookie(true); } $fragment = [ 'rhymixAdmin' => $isAdmin ? '1' : '0', ]; if ($isAdmin) { $fragment['rhymixAdminTicket'] = launcher_issue_admin_ticket($audience, $memberSrl); $fragment['rhymixAdminTicketExpiresIn'] = (string)LAUNCHER_ADMIN_TICKET_TTL; } header('Location: ' . $returnUrl . '#' . http_build_query($fragment, '', '&', PHP_QUERY_RFC3986), true, 303); exit; } if (isset($_GET['validateAdminTicket'])) { $audience = launcher_admin_ticket_audience((string)($_GET['aud'] ?? '')); $ticket = (string)($_GET['ticket'] ?? ''); $payload = launcher_validate_admin_ticket($ticket, $audience); $valid = is_array($payload); $response = [ 'valid' => $valid, 'admin' => $valid, ]; if ($valid) { $response['adminTicket'] = launcher_issue_admin_ticket($audience, (int)$payload['member_srl']); $response['adminTicketExpiresIn'] = LAUNCHER_ADMIN_TICKET_TTL; } custom_json($response); } if (isset($_GET['session'])) { $denied = custom_login_denied_state(); $blockedType = (string)($denied['type'] ?? ''); if (!empty($denied['denied']) && !($blockedType !== 'ip' && custom_is_site_admin())) { if ($blockedType === 'ip') { custom_clear_auth_cookie(); } elseif (function_exists('custom_clear_non_admin_auth_cookie')) { custom_clear_non_admin_auth_cookie(true); } else { custom_clear_auth_cookie(); } custom_json([ 'authenticated' => false, 'admin' => false, 'loginBlocked' => true, 'blockedType' => $blockedType, 'blockedMessage' => $denied['message'] ?? '현재 로그인할 수 없는 상태입니다.', ]); } if (custom_is_site_admin()) { custom_issue_auth_cookie(true); $pwaService = (string)($_GET['pwaService'] ?? ''); custom_json([ 'authenticated' => true, 'admin' => true, 'pwaAuthenticated' => $pwaService !== '' && function_exists('custom_has_pwa_auth_cookie') && custom_has_pwa_auth_cookie($pwaService), ]); } if (custom_has_auth_cookie()) { $pwaService = (string)($_GET['pwaService'] ?? ''); custom_json([ 'authenticated' => true, 'admin' => false, 'pwaAuthenticated' => $pwaService !== '' && function_exists('custom_has_pwa_auth_cookie') && custom_has_pwa_auth_cookie($pwaService), ]); } $pwaService = (string)($_GET['pwaService'] ?? ''); $pwaAuthenticated = $pwaService !== '' && function_exists('custom_has_pwa_auth_cookie') && custom_has_pwa_auth_cookie($pwaService); custom_json([ 'authenticated' => false, 'admin' => false, 'pwaAuthenticated' => $pwaAuthenticated, ]); } if (isset($_GET['adminSession'])) { $memberSrl = launcher_current_site_admin_member_srl(); $isAdmin = $memberSrl > 0; if ($isAdmin) { custom_issue_auth_cookie(true); } $response = [ 'admin' => $isAdmin, 'authenticated' => $isAdmin, ]; $audience = launcher_admin_ticket_audience((string)($_GET['adminTicket'] ?? '')); if ($isAdmin && $audience !== '') { $response['adminTicket'] = launcher_issue_admin_ticket($audience, $memberSrl); $response['adminTicketExpiresIn'] = LAUNCHER_ADMIN_TICKET_TTL; } custom_json($response); } if (isset($_GET['failMessages'])) { custom_json([ 'failMessages' => [ '비밀번호가 틀립니다.', ], ]); } if (isset($_GET['pokeCount'])) { $state = custom_ha_state('mokpo', $config['entities']['poke_count']); $count = $state['state'] ?? null; if ($count === null) { custom_json(['ok' => false, 'error' => 'failed_to_fetch_from_ha'], 502); } custom_json([ 'ok' => true, 'count' => is_numeric($count) ? (int)$count : $count, ]); } custom_json(['error' => 'not_found'], 404); } custom_json(['error' => 'method_not_allowed'], 405);