POSA_LEAKSMS/sql/migration_add_leak.sql
cw2007 73e49fd528 feat: RPi4 누수+온습도 2-모듈 단일 서버 통합 + 보안 하드닝
2번 모듈 신규(raspberry_pi4_sht30/): SHT30 온습도 → cafe24, raw-body X-Signature HMAC, 폐쇄망 오프라인 설치(vendor/install), systemd, CRC/포맷 자가점검. firmware sht30_fw wire 포맷과 byte-parity 검증.

1번 모듈 통합(raspberry_pi4/): 누수+카메라를 동일 서버로. 레거시 sha256(key+json) body-sig → X-Signature HMAC 전환, 사진 canonical=device_id|sensor_id|log_id|timestamp.

서버 통합(php/): api/sensor_data.php 누수 이벤트 수신, api/photo_upload.php 신규(HMAC·finfo·서버생성 파일명·nonce 멱등·sensor 결속), photo.php 인증 사진서빙, dashboard.php 누수 패널+사진, sms_send.php 누수 SMS, config.php 태그/요약.

DB: sql/migration_add_leak.sql(가산·멱등), schema_sht30.sql 통합 스키마.

보안 하드닝(외부 펜테스트 대비, 확정 CRITICAL/HIGH 0): cron_heartbeat api_key fail-closed, setup_mfa 부트스트랩 토큰 post-enrollment 잠금, var/ PHP 실행차단, HSTS.

문서/테스트: SYSTEM_OVERVIEW.md, security_hardening_test.php 갱신. 참고: 사용자 요청으로 세션 외 기존 미커밋 변경도 함께 포함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:21:11 +09:00

105 lines
5.6 KiB
SQL

-- =============================================================================
-- migration_add_leak.sql - SHT30 전용 서버에 누수(leak) 수신을 다시 추가(통합)
--
-- [목적] 1번 모듈(누수+카메라, sensor_id=1)과 2번 모듈(SHT30 온습도, sensor_id=2)이
-- "하나의 cafe24 서버 php/" 로 함께 보고하도록 통합한다. 이 마이그레이션은
-- sql/migration_drop_leak.sql 가 제거했던 누수 요소를 "가산(additive)·멱등"으로
-- 되돌린다(= migration_drop_leak.sql 의 역방향, 단 데이터 삭제 없이 가산만 수행).
--
-- [안전성] 운영 중인 SHT30 서버에서 그대로 실행해도 안전하다:
-- - sensor_metric(SHT30 측정) / sms_log / sensor_status / ingest_nonce 등 기존 테이블·
-- 데이터는 전혀 건드리지 않는다.
-- - 모든 변경은 "없을 때만 추가"(IF NOT EXISTS / information_schema 가드)이므로
-- 여러 번 재실행해도 오류가 나지 않는다.
--
-- [되돌리는 항목]
-- - sensor_log.event_type ENUM 에 leak_alert / leak_recovery 재추가
-- - sensor_log.is_leak 컬럼(없으면 추가)
-- - sensor_status.is_leak 컬럼(없으면 추가)
-- - leak_incident 테이블(없으면 생성) — 누수 사고(에피소드) 시작/복구 추적
-- - leak_photo 테이블(없으면 생성) — 누수 시 촬영 사진 메타데이터
--
-- 신규(통합) 설치라면 이 파일이 아니라 sql/schema_sht30.sql 하나만 실행하면 된다
-- (schema_sht30.sql 이 이미 누수 요소를 포함하도록 통합되어 있다).
-- =============================================================================
SET NAMES utf8mb4;
-- 1) sensor_log.event_type: 누수 enum 값을 다시 허용(멱등 — 동일 enum 재적용은 안전).
ALTER TABLE `sensor_log`
MODIFY `event_type` ENUM('startup','periodic','leak_alert','leak_recovery')
NOT NULL DEFAULT 'periodic';
-- 2~3) 컬럼/인덱스 추가는 "없을 때만" 수행해 재실행해도 오류가 없게 한다.
-- (MySQL/MariaDB 는 ADD COLUMN IF NOT EXISTS 호환이 일정치 않으므로
-- information_schema 로 가드하는 멱등 프로시저를 사용한다.)
DROP PROCEDURE IF EXISTS _mig_add_leak;
DELIMITER //
CREATE PROCEDURE _mig_add_leak()
BEGIN
DECLARE db VARCHAR(64);
SELECT DATABASE() INTO db;
-- sensor_log.is_leak: 누수 여부(0/1) — 없으면 추가
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_log' AND COLUMN_NAME = 'is_leak') THEN
ALTER TABLE `sensor_log`
ADD COLUMN `is_leak` TINYINT(1) NOT NULL DEFAULT 0
COMMENT '누수 여부 (1=누수, 0=정상)' AFTER `event_type`;
END IF;
-- sensor_status.is_leak: 최신 누수 상태(0/1) — 없으면 추가
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_status' AND COLUMN_NAME = 'is_leak') THEN
ALTER TABLE `sensor_status`
ADD COLUMN `is_leak` TINYINT(1) NOT NULL DEFAULT 0
COMMENT '최신 누수 상태 (1=누수, 0=정상)' AFTER `last_event_type`;
END IF;
END //
DELIMITER ;
CALL _mig_add_leak();
DROP PROCEDURE IF EXISTS _mig_add_leak;
-- 4) leak_incident: 누수 사고(에피소드) 추적. leak_alert 시 행을 열고(open), 같은
-- 센서의 leak_recovery 시 가장 최근 미복구 행을 닫는다(recovered_at 채움).
CREATE TABLE IF NOT EXISTS `leak_incident` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`sensor_id` TINYINT UNSIGNED NOT NULL COMMENT '센서 번호(누수=1)',
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '',
`device_location` VARCHAR(100) NOT NULL DEFAULT '',
`started_at` DATETIME NOT NULL COMMENT '누수 감지(시작) 시각',
`recovered_at` DATETIME NULL DEFAULT NULL COMMENT '누수 복구 시각(미복구면 NULL)',
`log_id_alert` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT 'leak_alert sensor_log.id',
`log_id_recovery` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT 'leak_recovery sensor_log.id',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_sensor_open` (`sensor_id`, `recovered_at`),
INDEX `idx_started_at` (`started_at`)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci
COMMENT='누수 사고(에피소드) 추적 (시작/복구)';
-- 5) leak_photo: 누수 시 촬영한 사진 메타데이터. 실제 파일은 php/var/photos/ 에 저장되고
-- filename 은 서버가 생성한 무작위 파일명(클라이언트 경로 미사용 — 경로주입 방지).
CREATE TABLE IF NOT EXISTS `leak_photo` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`log_id` BIGINT UNSIGNED NOT NULL COMMENT 'leak_alert sensor_log.id',
`sensor_id` TINYINT UNSIGNED NOT NULL,
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
`filename` VARCHAR(128) NOT NULL COMMENT '서버 생성 파일명(var/photos/ 하위)',
`mime` VARCHAR(40) NOT NULL DEFAULT 'image/jpeg',
`bytes` INT UNSIGNED NOT NULL DEFAULT 0,
`captured_at` DATETIME NOT NULL COMMENT '촬영(업로드) 시각',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_log_id` (`log_id`),
INDEX `idx_captured_at` (`captured_at`)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci
COMMENT='누수 사진 메타데이터 (파일은 var/photos/)';