Use shared auth and proxy Home Assistant webhooks
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# Find My Device
|
||||
|
||||
Home Assistant에 기록된 Fold7 위치, 배터리, 활동 상태, 이동 경로를 지도에 표시하는 개인용 나의 찾기 페이지입니다.
|
||||
|
||||
## 기능
|
||||
|
||||
- 현재 위치 지도 표시
|
||||
- GPS 정확도 원 표시
|
||||
- 최근 24시간 및 사용자 지정 기간 이동 경로 표시
|
||||
- 배터리, 워치 배터리, 활동 상태, 걸음 수, 이동 거리 표시
|
||||
- 자동 위치 갱신 웹훅 제어
|
||||
- 강제 업데이트 요청
|
||||
- 한국어/영어 전환
|
||||
- Wake Lock 지원
|
||||
|
||||
## 구조
|
||||
|
||||
- `debug.html`: 페이지 삽입용 HTML 조각
|
||||
- `php/api.php`: Home Assistant 상태/히스토리 조회 API
|
||||
- `js/main.js`: 지도 렌더링, 상태 갱신, 히스토리 표시
|
||||
- `css/style.css`: iOS 설정 화면 스타일
|
||||
- `img/`: 지도 마커 및 언어 아이콘
|
||||
|
||||
## 공통 모듈 연동
|
||||
|
||||
인증과 Home Assistant 호출은 `/custom/common`을 사용합니다. 브라우저 JS에는 더 이상 API Bearer 토큰이나 Home Assistant 웹훅 식별자를 직접 넣지 않습니다.
|
||||
|
||||
## 운영 메모
|
||||
|
||||
이 프로젝트는 위치, 주소, 배터리, 활동 기록을 포함하므로 private 저장소와 개인 접근 환경에서만 운영합니다.
|
||||
+17
-7
@@ -413,9 +413,9 @@ document.getElementById('autoUpdateSwitch').addEventListener('change', () => {
|
||||
const isChecked = document.getElementById('autoUpdateSwitch')?.checked;
|
||||
|
||||
if (isChecked) {
|
||||
fetch('https://ha.chaegeon.com/api/webhook/5P1HgWiDpaAK8E0B', { method: 'POST' });
|
||||
callDeviceWebhook('auto_on');
|
||||
} else {
|
||||
fetch('https://ha.chaegeon.com/api/webhook/z2nHt75jRxWKo2zF', { method: 'POST' });
|
||||
callDeviceWebhook('auto_off');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -426,7 +426,7 @@ document.getElementById('autoUpdateSwitch').addEventListener('change', () => {
|
||||
const isAutoUpdateOn = switchEl?.checked;
|
||||
|
||||
if (isAutoUpdateOn) {
|
||||
navigator.sendBeacon('https://ha.chaegeon.com/api/webhook/z2nHt75jRxWKo2zF');
|
||||
callDeviceWebhook('auto_off', true);
|
||||
|
||||
// 스위치 UI 강제 끔
|
||||
switchEl.checked = false;
|
||||
@@ -437,7 +437,7 @@ document.getElementById('autoUpdateSwitch').addEventListener('change', () => {
|
||||
|
||||
setInterval(() => {
|
||||
if (document.getElementById('autoUpdateSwitch')?.checked) {
|
||||
fetch('https://ha.chaegeon.com/api/webhook/870ZNzB20WRkzjnh', { method: 'POST' });
|
||||
callDeviceWebhook('auto_tick');
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
@@ -984,9 +984,9 @@ function fetchDeviceData(includeLang = true, range = null) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'REDACTED_HA_TOKEN'
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({})
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -1020,7 +1020,7 @@ function resetDisplayValues() {
|
||||
}
|
||||
|
||||
function triggerForceUpdate() {
|
||||
fetch('https://ha.chaegeon.com/api/webhook/g1M4dZrsiLTt1woqgDXL6wtBSNCp6HGsRSzRLnNV4Up2Tpy8kre0q33EGvB70H9ftw48oENbUngHhVEBRlBFLNA7x7MbcmwZnVSHU71BG83ROMy4MOqIDWGxpKvI1YPX', { method: 'POST' });
|
||||
callDeviceWebhook('force_update');
|
||||
|
||||
const el = document.getElementById('updateBtn');
|
||||
if (!el) return;
|
||||
@@ -1072,3 +1072,13 @@ function renderTrackFromDisplayedMarkers() {
|
||||
mapState.trackLine.setPath(points);
|
||||
}
|
||||
}
|
||||
|
||||
function callDeviceWebhook(name, keepalive = false) {
|
||||
return fetch('https://chaegeon.com/custom/findmydevice/php/api.php?action=webhook', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
keepalive,
|
||||
body: JSON.stringify({ name })
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
+53
-86
@@ -1,11 +1,6 @@
|
||||
<?php
|
||||
// Apache 환경 차단
|
||||
if (isset($_SERVER['SERVER_SOFTWARE']) && stripos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Access forbidden: Apache environment is not allowed']);
|
||||
exit;
|
||||
}
|
||||
require_once __DIR__ . '/../../common/auth.php';
|
||||
require_once __DIR__ . '/../../common/ha.php';
|
||||
|
||||
// POST 전용 검사
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
@@ -15,69 +10,47 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// JSON 요청 검사
|
||||
$content_type = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
if (stripos($content_type, 'application/json') === false) {
|
||||
http_response_code(415); // Unsupported Media Type
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Unsupported Content-Type. application/json required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
function get_authorization_header() {
|
||||
if (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||
return trim($_SERVER['HTTP_AUTHORIZATION']);
|
||||
} elseif (function_exists('apache_request_headers')) {
|
||||
foreach (apache_request_headers() as $key => $value) {
|
||||
if (strcasecmp($key, 'Authorization') === 0) {
|
||||
return trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$auth_header = get_authorization_header();
|
||||
if (!$auth_header || !preg_match('/^Bearer\s+(\S+)$/', $auth_header, $matches)) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Unauthorized: Missing or invalid Authorization header']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$allowed_password = 'XBv9z1IGqPUdtVf7DABFA16H0E154LJS';
|
||||
$password = $matches[1];
|
||||
if ($password !== $allowed_password) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Unauthorized: Invalid token']);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Expires: 0');
|
||||
|
||||
$baseUrl = "https://ha.seoul.chaegeon.com/api/states";
|
||||
$headers = [
|
||||
"Authorization: Bearer REDACTED_HA_TOKEN",
|
||||
"Content-Type: application/json"
|
||||
custom_require_auth();
|
||||
|
||||
$requestData = custom_read_request_data();
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'webhook') {
|
||||
$name = (string)($requestData['name'] ?? '');
|
||||
$allowed = [
|
||||
'auto_on' => 'find_auto_on',
|
||||
'auto_off' => 'find_auto_off',
|
||||
'auto_tick' => 'find_auto_tick',
|
||||
'force_update' => 'find_force_update',
|
||||
];
|
||||
|
||||
if (!isset($allowed[$name])) {
|
||||
custom_json(['result' => 'invalid_webhook'], 400);
|
||||
}
|
||||
|
||||
$ok = custom_ha_webhook($allowed[$name]);
|
||||
custom_json(['result' => $ok ? 'ok' : 'fail'], $ok ? 200 : 502);
|
||||
}
|
||||
|
||||
$config = custom_config();
|
||||
$entities = $config['entities'];
|
||||
|
||||
// 엔드포인트 URL
|
||||
$deviceUrl = "$baseUrl/device_tracker.seocaegeonyi_z_fold7";
|
||||
$geoUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_geocoded_location";
|
||||
$batteryStateUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_battery_state";
|
||||
$batteryLevelUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_battery_level";
|
||||
$watchBatteryStateUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_watch_battery_state";
|
||||
$watchBatteryLevelUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_watch_battery_level";
|
||||
$activityUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_detected_activity";
|
||||
$connectionUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_network_type";
|
||||
$fascendedUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_daily_floors";
|
||||
$fdescendedUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_daily_floors";
|
||||
$stepsUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_daily_steps";
|
||||
$distanceUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_daily_distance";
|
||||
$triggerUrl = "$baseUrl/sensor.seocaegeonyi_z_fold7_last_update_trigger";
|
||||
$deviceUrl = $entities['device'];
|
||||
$geoUrl = $entities['geo'];
|
||||
$batteryStateUrl = $entities['battery_state'];
|
||||
$batteryLevelUrl = $entities['battery_level'];
|
||||
$watchBatteryStateUrl = $entities['watch_battery_state'];
|
||||
$watchBatteryLevelUrl = $entities['watch_battery_level'];
|
||||
$activityUrl = $entities['activity'];
|
||||
$connectionUrl = $entities['connection'];
|
||||
$fascendedUrl = $entities['daily_floors'];
|
||||
$fdescendedUrl = $entities['daily_floors'];
|
||||
$stepsUrl = $entities['daily_steps'];
|
||||
$distanceUrl = $entities['daily_distance'];
|
||||
$triggerUrl = $entities['trigger'];
|
||||
|
||||
// GET 파라미터 읽기
|
||||
$startParam = $_GET['startTime'] ?? null;
|
||||
@@ -97,34 +70,28 @@ if ($startParam && $endParam) {
|
||||
}
|
||||
|
||||
// API URL 생성
|
||||
$historyUrl = "https://ha.seoul.chaegeon.com/api/history/period/{$startTime}?filter_entity_id=device_tracker.seocaegeonyi_z_fold7&end_time={$endTime}";
|
||||
$historyEndpoint = "/history/period/{$startTime}?filter_entity_id=" . rawurlencode($entities['device']) . "&end_time={$endTime}";
|
||||
|
||||
// 공통 요청 함수
|
||||
function getData($url, $headers) {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return json_decode($response, true);
|
||||
function getData($entityId) {
|
||||
return custom_ha_state('seoul', $entityId) ?? [];
|
||||
}
|
||||
|
||||
// 데이터 불러오기
|
||||
$deviceData = getData($deviceUrl, $headers);
|
||||
$geoData = getData($geoUrl, $headers);
|
||||
$batteryStateData = getData($batteryStateUrl, $headers);
|
||||
$batteryLevelData = getData($batteryLevelUrl, $headers);
|
||||
$watchBatteryStateData = getData($watchBatteryStateUrl, $headers);
|
||||
$watchBatteryLevelData = getData($watchBatteryLevelUrl, $headers);
|
||||
$activityData = getData($activityUrl, $headers);
|
||||
$connectionData = getData($connectionUrl, $headers);
|
||||
$ascendedData = getData($fascendedUrl, $headers);
|
||||
$descendedData = getData($fdescendedUrl, $headers);
|
||||
$stepsData = getData($stepsUrl, $headers);
|
||||
$distanceData = getData($distanceUrl, $headers);
|
||||
$historyData = getData($historyUrl, $headers);
|
||||
$triggerData = getData($triggerUrl, $headers);
|
||||
$deviceData = getData($deviceUrl);
|
||||
$geoData = getData($geoUrl);
|
||||
$batteryStateData = getData($batteryStateUrl);
|
||||
$batteryLevelData = getData($batteryLevelUrl);
|
||||
$watchBatteryStateData = getData($watchBatteryStateUrl);
|
||||
$watchBatteryLevelData = getData($watchBatteryLevelUrl);
|
||||
$activityData = getData($activityUrl);
|
||||
$connectionData = getData($connectionUrl);
|
||||
$ascendedData = getData($fascendedUrl);
|
||||
$descendedData = getData($fdescendedUrl);
|
||||
$stepsData = getData($stepsUrl);
|
||||
$distanceData = getData($distanceUrl);
|
||||
$historyData = custom_ha_request('seoul', 'GET', $historyEndpoint) ?? [];
|
||||
$triggerData = getData($triggerUrl);
|
||||
|
||||
// 경로 기록
|
||||
$path = [];
|
||||
|
||||
Reference in New Issue
Block a user