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>
167 lines
9.1 KiB
SQL
167 lines
9.1 KiB
SQL
-- =============================================================================
|
|
-- schema_sht30.sql - 통합 모니터링 시스템 신규 설치용 스키마
|
|
-- (1번 모듈=누수+카메라 sensor_id=1, 2번 모듈=SHT30 온습도 sensor_id=2 — 단일 서버)
|
|
--
|
|
-- 신규 설치: 이 파일 하나만 실행합니다.
|
|
-- SOURCE /path/to/sql/schema_sht30.sql;
|
|
--
|
|
-- 기존(누수 v2605) 설치를 정리·재설치할 때는 운영 데이터를 보존하려면 마이그레이션을
|
|
-- 사용합니다: 누수를 빼려면 sql/migration_drop_leak.sql, 빠진 서버에 누수를 다시
|
|
-- 더하려면 sql/migration_add_leak.sql(가산·멱등).
|
|
--
|
|
-- 이 파일은 두 모듈을 하나의 서버에서 받는 "통합" 스키마입니다. 포함 요소:
|
|
-- - 온습도(SHT30): sensor_metric 테이블 + 서버 임계 판정(config.php METRIC_*).
|
|
-- - 누수(leak): sensor_log.is_leak / sensor_status.is_leak 컬럼,
|
|
-- event_type 의 leak_alert / leak_recovery,
|
|
-- leak_incident(사고 추적) + leak_photo(사진 메타) 테이블.
|
|
-- 임계/누수 경보 SMS 발송은 서버 PHP(api/sensor_data.php + sms_send.php)에서 수행하며,
|
|
-- 발송 이력은 sms_log 에 남깁니다(sms_log 기준 쿨다운/복구 판정 — 별도 경보 상태 테이블 없음).
|
|
-- 누수 사진 실제 파일은 php/var/photos/ 에 저장되고, leak_photo 에는 메타만 둡니다.
|
|
-- =============================================================================
|
|
|
|
SET NAMES utf8mb4;
|
|
SET time_zone = '+09:00';
|
|
|
|
CREATE TABLE IF NOT EXISTS `sensor_log` (
|
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
`device_id` VARCHAR(64) NOT NULL COMMENT 'STM32 기기 ID',
|
|
`device_location` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '설치 위치',
|
|
`sensor_id` TINYINT UNSIGNED NOT NULL COMMENT '센서 번호',
|
|
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '센서 이름',
|
|
`event_type` ENUM('startup','periodic','leak_alert','leak_recovery')
|
|
NOT NULL DEFAULT 'periodic',
|
|
`is_leak` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '누수 여부 (1=누수, 0=정상)',
|
|
`sensor_time` DATETIME NOT NULL COMMENT '센서 측정 시각 (기기 기준)',
|
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'DB 저장 시각',
|
|
|
|
PRIMARY KEY (`id`),
|
|
INDEX `idx_sensor_time` (`sensor_time`),
|
|
INDEX `idx_device_sensor` (`device_id`, `sensor_id`),
|
|
INDEX `idx_event_type` (`event_type`, `created_at`)
|
|
) ENGINE=InnoDB
|
|
DEFAULT CHARSET=utf8mb4
|
|
COLLATE=utf8mb4_unicode_ci
|
|
COMMENT='센서 이벤트 전체 로그';
|
|
|
|
CREATE TABLE IF NOT EXISTS `sensor_status` (
|
|
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
|
`device_id` VARCHAR(64) NOT NULL,
|
|
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
`device_location` VARCHAR(100) NOT NULL DEFAULT '',
|
|
`last_event_type` VARCHAR(20) NOT NULL DEFAULT '',
|
|
`is_leak` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '최신 누수 상태 (1=누수, 0=정상)',
|
|
`last_seen` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
ON UPDATE CURRENT_TIMESTAMP,
|
|
`offline_alerted` TINYINT(1) NOT NULL DEFAULT 0
|
|
COMMENT '오프라인 알림 발송 여부 (1=발송됨, 0=미발송)',
|
|
|
|
PRIMARY KEY (`sensor_id`)
|
|
) ENGINE=InnoDB
|
|
DEFAULT CHARSET=utf8mb4
|
|
COLLATE=utf8mb4_unicode_ci
|
|
COMMENT='센서별 최신 상태 (업서트)';
|
|
|
|
CREATE TABLE IF NOT EXISTS `sensor_metric` (
|
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
|
|
`device_location` VARCHAR(100) NOT NULL DEFAULT '',
|
|
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
|
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
`metric_type` VARCHAR(30) NOT NULL DEFAULT 'sht30',
|
|
`temperature_c` DECIMAL(6,2) NULL,
|
|
`humidity_percent` DECIMAL(5,2) NULL,
|
|
`metric_status` VARCHAR(30) NOT NULL DEFAULT 'normal'
|
|
COMMENT '서버 임계 판정 결과: normal/high_temp/low_temp/high_humidity/low_humidity/condensation_risk',
|
|
`measured_at` DATETIME NOT NULL,
|
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
PRIMARY KEY (`id`),
|
|
INDEX `idx_sensor_measured` (`sensor_id`, `measured_at`),
|
|
INDEX `idx_device_sensor` (`device_id`, `sensor_id`),
|
|
INDEX `idx_metric_type` (`metric_type`, `created_at`),
|
|
INDEX `idx_status_measured` (`metric_status`, `measured_at`)
|
|
) ENGINE=InnoDB
|
|
DEFAULT CHARSET=utf8mb4
|
|
COLLATE=utf8mb4_unicode_ci
|
|
COMMENT='SHT30 온습도 측정 이력 (온도/습도/임계 상태)';
|
|
|
|
CREATE TABLE IF NOT EXISTS `sms_log` (
|
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
|
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
|
|
`message` TEXT NOT NULL,
|
|
`receivers` VARCHAR(500) NOT NULL COMMENT '쉼표 구분 수신자 번호',
|
|
`status` ENUM('success','fail') NOT NULL,
|
|
`result_code` VARCHAR(20) NOT NULL DEFAULT '',
|
|
`result_message` VARCHAR(255) NOT NULL DEFAULT '',
|
|
`sent_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
PRIMARY KEY (`id`),
|
|
INDEX `idx_sent_at` (`sent_at`),
|
|
INDEX `idx_sensor_sent` (`sensor_id`, `sent_at`)
|
|
) ENGINE=InnoDB
|
|
DEFAULT CHARSET=utf8mb4
|
|
COLLATE=utf8mb4_unicode_ci
|
|
COMMENT='SMS 발송 이력 (온습도 임계 경보/복구, 장비 오프라인/복구)';
|
|
|
|
-- 수집 재전송(replay) 멱등 nonce. api/sensor_data.php 가 인증·신선도를 통과한 요청의
|
|
-- 서명(X-Signature)을 여기에 원자적으로 기록한다. 시계오차 창(INGEST_MAX_CLOCK_SKEW_SEC)
|
|
-- 안에서 같은 서명 본문이 다시 오면(펌웨어 재시도/캡처 재생) 중복 저장·중복 SMS 없이
|
|
-- 멱등 처리된다. 오래된 행은 서버가 주기적으로 정리한다(이 테이블이 없어도 수집은
|
|
-- 동작하며 멱등 방어만 비활성화된다 — graceful degrade).
|
|
CREATE TABLE IF NOT EXISTS `ingest_nonce` (
|
|
`signature` VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL
|
|
COMMENT '요청 서명(X-Signature) — 멱등 키',
|
|
`seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
COMMENT '최초 수신 시각(정리 기준)',
|
|
|
|
PRIMARY KEY (`signature`),
|
|
INDEX `idx_seen_at` (`seen_at`)
|
|
) ENGINE=InnoDB
|
|
DEFAULT CHARSET=utf8mb4
|
|
COLLATE=utf8mb4_unicode_ci
|
|
COMMENT='수집 재전송 멱등 nonce (시계오차 창 내 중복 차단; 주기적 정리)';
|
|
|
|
-- 누수 사고(에피소드) 추적. leak_alert 시 행을 열고(open), 같은 센서의 leak_recovery
|
|
-- 시 가장 최근 미복구(recovered_at IS NULL) 행을 닫는다. 대시보드의 누수 상태/이력 표시에 사용.
|
|
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='누수 사고(에피소드) 추적 (시작/복구)';
|
|
|
|
-- 누수 사진 메타데이터. 실제 파일은 php/var/photos/ 에 서버 생성 무작위 파일명으로 저장
|
|
-- (클라이언트 파일명/경로 미사용 — 경로주입 방지). api/photo_upload.php 가 기록한다.
|
|
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/)';
|