nginx 의 정규식 location 은 프리픽스 location 보다 먼저 매칭되므로, `location ~* \.(js|css|json)$` 블록이 있는 서버에서는 확장자 붙은 동적 엔드포인트가 `try_files ... /index.php` 폴백이 실행될 기회 없이 404 가 된다. aaPanel/CyberPanel/Plesk 기본 템플릿에 들어있어 드물지 않으며, 관리자 화면조차 뜨지 않아 "서버 설정을 고치세요" 안내가 순환 참조가 된다. 관례를 깬 쪽이 G7 이므로 해소 책임도 G7 에 두고, 두 형태를 모두 서빙한 뒤 환경에 따라 택일한다. 확장자 형태는 영구 유지한다 — 제거하면 URL 을 하드코딩한 서드파티 확장이 깨진다. - 라우트: dualSuffix / dualSuffixSegment / dualAsset 매크로로 23개 엔드포인트와 프로브를 이중 등록. 확장자 형태를 먼저 등록한다 — 확장자 없는 쪽이 더 느슨한 패턴이라 순서가 뒤집히면 `.json` 요청까지 삼킨다 - URL 생성: 서버 App\Support\AssetUrl, 프론트 core/support/assetUrl.ts 로 집약. 기본 모드에서 생성 결과는 치환 이전과 문자열까지 동일하다 (쿼리 순서 포함 — 순서가 바뀌면 의미는 같아도 HTTP 캐시 키가 갈린다) - 자가 복구: 부트스트랩 자산 로드 실패 시 확장자 없는 형태로 1회 단방향 전환. 역방향 금지 + 기존 재시도 예산 공유로 무한 왕복을 막는다 - 감지·전환: 인스톨러 프로브(설치 시 확정) / 관리자 환경설정 일반 탭 / g7:asset-url-mode. 판정은 상태코드가 아니라 매직 토큰 + Content-Type 으로 한다. 상태코드만 보면 "404 대신 200 + 에러 HTML" 환경에서 영원히 오판한다 - 봇은 JavaScript 를 실행하지 않아 자가 복구가 닿지 않으므로, 모드 변경 시 SEO 프리렌더 캐시를 비워 재생성시킨다 가드: audit 룰 2종(dynamic-route-static-extension / asset-url-builder-required), Playwright 6건(정적 블록 가로채기 시뮬레이션), 루프 방지 불변식 L1~L9 전수 red 증명. 부수 정리: 양 Composer 에 중복돼 있던 확장 에셋 수집 123줄을 트레이트로 통합하고, 자산 확장자 화이트리스트 누락(.map)과 경로 정규화 우회 가능성을 함께 교정. sir.kr 커뮤니티의 hang 님께서 제보해주셨습니다.
339 lines
17 KiB
JSON
339 lines
17 KiB
JSON
{
|
|
"_meta": {
|
|
"version": "1.0.0",
|
|
"description": "그누보드7 환경설정 기본값 및 프론트엔드 스키마",
|
|
"categories": ["general", "security", "mail", "upload", "seo", "cache", "debug", "drivers", "core_update", "geoip", "notifications", "identity"]
|
|
},
|
|
"defaults": {
|
|
"general": {
|
|
"site_name": "그누보드7",
|
|
"site_url": "",
|
|
"site_description": "",
|
|
"admin_email": "",
|
|
"timezone": "Asia/Seoul",
|
|
"language": "ko",
|
|
"currency": "KRW",
|
|
"maintenance_mode": false,
|
|
"site_logo": [],
|
|
"asset_url_mode": "extension"
|
|
},
|
|
"security": {
|
|
"force_https": false,
|
|
"login_attempt_enabled": true,
|
|
"auth_token_lifetime": 30,
|
|
"max_login_attempts": 5,
|
|
"login_lockout_time": 5,
|
|
"two_factor_auth": false,
|
|
"password_min_length": 8,
|
|
"require_password_special_char": false,
|
|
"allow_internal_outbound_urls": false
|
|
},
|
|
"mail": {
|
|
"mailer": "smtp",
|
|
"host": "",
|
|
"port": 587,
|
|
"username": "",
|
|
"password": "",
|
|
"encryption": "tls",
|
|
"mailgun_domain": "",
|
|
"mailgun_secret": "",
|
|
"mailgun_endpoint": "api.mailgun.net",
|
|
"ses_key": "",
|
|
"ses_secret": "",
|
|
"ses_region": "ap-northeast-2",
|
|
"from_address": "noreply@example.com",
|
|
"from_name": "그누보드7"
|
|
},
|
|
"upload": {
|
|
"max_file_size": 10,
|
|
"allowed_extensions": ["jpg", "jpeg", "png", "gif", "webp", "pdf", "doc", "docx", "xls", "xlsx", "zip"],
|
|
"image_max_width": 2000,
|
|
"image_max_height": 2000,
|
|
"image_quality": 85
|
|
},
|
|
"seo": {
|
|
"meta_title_suffix": "",
|
|
"meta_description": "",
|
|
"meta_keywords": "",
|
|
"google_analytics_id": "",
|
|
"google_site_verification": "",
|
|
"naver_site_verification": "",
|
|
"bot_user_agents": ["kakaotalk-scrap", "Meta-ExternalAgent", "ChatGPT-User"],
|
|
"bot_detection_enabled": true,
|
|
"bot_detection_library_enabled": true,
|
|
"og_default_site_name": "",
|
|
"og_image_default_width": 1200,
|
|
"og_image_default_height": 630,
|
|
"twitter_default_card": "summary_large_image",
|
|
"twitter_default_site": "",
|
|
"cache_enabled": null,
|
|
"cache_ttl": null,
|
|
"sitemap_enabled": true,
|
|
"sitemap_cache_ttl": null,
|
|
"sitemap_urls_per_file": 50000,
|
|
"sitemap_gzip": false,
|
|
"sitemap_serve_stale_on_miss": true,
|
|
"sitemap_max_urls_per_contributor": 0,
|
|
"sitemap_hreflang_enabled": true,
|
|
"sitemap_schedule": "daily",
|
|
"sitemap_schedule_time": "02:00",
|
|
"sitemap_last_updated_at": "",
|
|
"generator_enabled": true,
|
|
"generator_content": ""
|
|
},
|
|
"cache": {
|
|
"enabled": true,
|
|
"default_ttl": 86400,
|
|
"layout_enabled": true,
|
|
"layout_ttl": 3600,
|
|
"seo_enabled": true,
|
|
"seo_ttl": 7200,
|
|
"seo_sitemap_ttl": 86400,
|
|
"stats_enabled": true,
|
|
"stats_ttl": 1800,
|
|
"notification_ttl": 3600,
|
|
"extension_status_ttl": 86400,
|
|
"geoip_enabled": true,
|
|
"geoip_ttl": 86400,
|
|
"version_check_ttl": 3600
|
|
},
|
|
"debug": {
|
|
"mode": false,
|
|
"sql_query_log": false,
|
|
"log_level": "error"
|
|
},
|
|
"core_update": {
|
|
"github_url": "",
|
|
"github_token": ""
|
|
},
|
|
"geoip": {
|
|
"feature_enabled": false,
|
|
"license_key": "",
|
|
"auto_update_enabled": true,
|
|
"last_updated_at": ""
|
|
},
|
|
"drivers": {
|
|
"storage_driver": "local",
|
|
"s3_bucket": "",
|
|
"s3_region": "ap-northeast-2",
|
|
"s3_access_key": "",
|
|
"s3_secret_key": "",
|
|
"s3_url": "",
|
|
"cache_driver": "file",
|
|
"redis_host": "127.0.0.1",
|
|
"redis_port": 6379,
|
|
"redis_password": "",
|
|
"redis_database": 0,
|
|
"memcached_host": "127.0.0.1",
|
|
"memcached_port": 11211,
|
|
"session_driver": "file",
|
|
"session_lifetime": 120,
|
|
"queue_driver": "sync",
|
|
"log_driver": "daily",
|
|
"log_level": "error",
|
|
"log_days": 14,
|
|
"websocket_enabled": false,
|
|
"websocket_app_id": "",
|
|
"websocket_app_key": "",
|
|
"websocket_app_secret": "",
|
|
"websocket_host": "localhost",
|
|
"websocket_port": 8080,
|
|
"websocket_scheme": "https",
|
|
"websocket_verify_ssl": true,
|
|
"websocket_server_host": "127.0.0.1",
|
|
"websocket_server_port": 8080,
|
|
"websocket_server_scheme": "http",
|
|
"search_engine_driver": "mysql-fulltext"
|
|
},
|
|
"notifications": {
|
|
"channels": [
|
|
{ "id": "mail", "is_active": true, "sort_order": 1 },
|
|
{ "id": "database", "is_active": true, "sort_order": 2 }
|
|
]
|
|
},
|
|
"identity": {
|
|
"default_provider": "g7:core.mail",
|
|
"purpose_providers": {
|
|
"password_reset": null,
|
|
"self_update": null,
|
|
"sensitive_action": null
|
|
},
|
|
"challenge_ttl_minutes": 15,
|
|
"max_attempts": 5
|
|
}
|
|
},
|
|
"frontend_schema": {
|
|
"_description": "프론트엔드에 노출할 설정 필드 정의. 각 카테고리별로 노출할 필드와 타입 캐스팅, 민감 정보 여부를 정의합니다.",
|
|
"general": {
|
|
"expose": true,
|
|
"_comment": "site_url, admin_email, language, timezone는 프론트엔드에서 미참조하므로 expose: false",
|
|
"fields": {
|
|
"site_name": { "type": "string", "sensitive": false },
|
|
"site_url": { "type": "string", "sensitive": false, "expose": false },
|
|
"site_description": { "type": "string", "sensitive": false },
|
|
"admin_email": { "type": "string", "sensitive": false, "expose": false },
|
|
"language": { "type": "string", "sensitive": false, "expose": false },
|
|
"timezone": { "type": "string", "sensitive": false, "expose": false },
|
|
"site_logo": { "type": "array", "sensitive": false, "transform": "attachments" },
|
|
"site_logo_url": { "type": "string", "sensitive": false, "computed": true, "source": "site_logo", "transform": "first_image_url" },
|
|
"asset_url_mode": { "type": "string", "sensitive": false }
|
|
}
|
|
},
|
|
"security": {
|
|
"expose": false,
|
|
"_comment": "보안 정책은 프론트엔드에서 미참조 — 노출 시 공격자에게 정보 제공 위험"
|
|
},
|
|
"mail": {
|
|
"expose": false,
|
|
"_comment": "mail 설정은 password 포함으로 프론트엔드에 노출하지 않음"
|
|
},
|
|
"upload": {
|
|
"expose": true,
|
|
"fields": {
|
|
"max_file_size": { "type": "integer", "sensitive": false },
|
|
"allowed_extensions": { "type": "string", "sensitive": false, "transform": "join_comma", "expose": false },
|
|
"allowed_extensions_formatted": { "type": "string", "sensitive": false, "computed": true, "source": "allowed_extensions", "transform": "format_extensions" },
|
|
"image_max_width": { "type": "integer", "sensitive": false },
|
|
"image_max_height": { "type": "integer", "sensitive": false },
|
|
"image_quality": { "type": "integer", "sensitive": false, "expose": false }
|
|
}
|
|
},
|
|
"seo": {
|
|
"expose": false,
|
|
"_comment": "SEO 설정 전체 — 프론트엔드 미참조 (admin 환경설정 폼은 API 데이터 기반)",
|
|
"fields": {
|
|
"meta_title_suffix": { "type": "string", "sensitive": false },
|
|
"meta_description": { "type": "string", "sensitive": false },
|
|
"meta_keywords": { "type": "string", "sensitive": false },
|
|
"google_analytics_id": { "type": "string", "sensitive": false },
|
|
"google_site_verification": { "type": "string", "sensitive": false },
|
|
"naver_site_verification": { "type": "string", "sensitive": false },
|
|
"bot_user_agents": { "type": "array", "sensitive": false },
|
|
"bot_detection_enabled": { "type": "boolean", "sensitive": false },
|
|
"bot_detection_library_enabled": { "type": "boolean", "sensitive": false },
|
|
"og_default_site_name": { "type": "string", "sensitive": false },
|
|
"og_image_default_width": { "type": "integer", "sensitive": false },
|
|
"og_image_default_height": { "type": "integer", "sensitive": false },
|
|
"twitter_default_card": { "type": "string", "sensitive": false },
|
|
"twitter_default_site": { "type": "string", "sensitive": false },
|
|
"cache_enabled": { "type": "boolean", "sensitive": false },
|
|
"cache_ttl": { "type": "integer", "sensitive": false },
|
|
"sitemap_enabled": { "type": "boolean", "sensitive": false },
|
|
"sitemap_cache_ttl": { "type": "integer", "sensitive": false },
|
|
"sitemap_urls_per_file": { "type": "integer", "sensitive": false },
|
|
"sitemap_gzip": { "type": "boolean", "sensitive": false },
|
|
"sitemap_serve_stale_on_miss": { "type": "boolean", "sensitive": false },
|
|
"sitemap_max_urls_per_contributor": { "type": "integer", "sensitive": false },
|
|
"sitemap_hreflang_enabled": { "type": "boolean", "sensitive": false },
|
|
"sitemap_schedule": { "type": "string", "sensitive": false },
|
|
"sitemap_schedule_time": { "type": "string", "sensitive": false },
|
|
"sitemap_last_updated_at": { "type": "string", "sensitive": false },
|
|
"generator_enabled": { "type": "boolean", "sensitive": false },
|
|
"generator_content": { "type": "string", "sensitive": false }
|
|
}
|
|
},
|
|
"cache": {
|
|
"expose": true,
|
|
"frontend_name": "advanced",
|
|
"_comment": "cache와 debug를 합쳐서 advanced로 노출",
|
|
"fields": {
|
|
"enabled": { "type": "boolean", "sensitive": false, "frontend_key": "cache_enabled", "expose": false },
|
|
"default_ttl": { "type": "integer", "sensitive": false, "frontend_key": "cache_default_ttl", "expose": false },
|
|
"layout_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "layout_cache_enabled", "expose": false },
|
|
"layout_ttl": { "type": "integer", "sensitive": false, "frontend_key": "layout_cache_ttl", "expose": false },
|
|
"seo_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "seo_cache_enabled", "expose": false },
|
|
"seo_ttl": { "type": "integer", "sensitive": false, "frontend_key": "seo_cache_ttl", "expose": false },
|
|
"seo_sitemap_ttl": { "type": "integer", "sensitive": false, "frontend_key": "seo_sitemap_cache_ttl", "expose": false },
|
|
"stats_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "stats_cache_enabled", "expose": false },
|
|
"stats_ttl": { "type": "integer", "sensitive": false, "frontend_key": "stats_cache_ttl", "expose": false },
|
|
"notification_ttl": { "type": "integer", "sensitive": false, "frontend_key": "notification_cache_ttl", "expose": false },
|
|
"extension_status_ttl": { "type": "integer", "sensitive": false, "frontend_key": "extension_status_cache_ttl", "expose": false },
|
|
"geoip_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "geoip_cache_enabled", "expose": false },
|
|
"geoip_ttl": { "type": "integer", "sensitive": false, "frontend_key": "geoip_cache_ttl", "expose": false },
|
|
"version_check_ttl": { "type": "integer", "sensitive": false, "frontend_key": "version_check_cache_ttl", "expose": false }
|
|
}
|
|
},
|
|
"debug": {
|
|
"expose": true,
|
|
"merge_into": "advanced",
|
|
"_comment": "debug 설정은 advanced 카테고리에 병합",
|
|
"fields": {
|
|
"mode": { "type": "boolean", "sensitive": false, "frontend_key": "debug_mode" },
|
|
"sql_query_log": { "type": "boolean", "sensitive": false, "expose": false }
|
|
}
|
|
},
|
|
"core_update": {
|
|
"expose": true,
|
|
"merge_into": "advanced",
|
|
"_comment": "core_update 설정은 advanced 카테고리에 병합",
|
|
"fields": {
|
|
"github_url": { "type": "string", "sensitive": false, "frontend_key": "core_update_github_url", "expose": false },
|
|
"github_token": { "type": "string", "sensitive": true, "frontend_key": "core_update_github_token" }
|
|
}
|
|
},
|
|
"geoip": {
|
|
"expose": true,
|
|
"merge_into": "advanced",
|
|
"_comment": "GeoIP 기능 설정은 advanced 카테고리에 병합",
|
|
"fields": {
|
|
"feature_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "geoip_enabled" },
|
|
"license_key": { "type": "string", "sensitive": true, "frontend_key": "geoip_license_key" },
|
|
"auto_update_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "geoip_auto_update_enabled" },
|
|
"last_updated_at": { "type": "string", "sensitive": false, "frontend_key": "geoip_last_updated_at" }
|
|
}
|
|
},
|
|
"drivers": {
|
|
"expose": false,
|
|
"_comment": "드라이버 설정 — 프론트엔드 미노출 (admin_settings에서 API 원본값 기반 비교로 변경)",
|
|
"fields": {
|
|
"storage_driver": { "type": "string", "sensitive": false },
|
|
"s3_bucket": { "type": "string", "sensitive": false },
|
|
"s3_region": { "type": "string", "sensitive": false },
|
|
"s3_access_key": { "type": "string", "sensitive": true },
|
|
"s3_secret_key": { "type": "string", "sensitive": true },
|
|
"s3_url": { "type": "string", "sensitive": false },
|
|
"cache_driver": { "type": "string", "sensitive": false },
|
|
"redis_host": { "type": "string", "sensitive": false },
|
|
"redis_port": { "type": "integer", "sensitive": false },
|
|
"redis_password": { "type": "string", "sensitive": true },
|
|
"redis_database": { "type": "integer", "sensitive": false },
|
|
"memcached_host": { "type": "string", "sensitive": false },
|
|
"memcached_port": { "type": "integer", "sensitive": false },
|
|
"session_driver": { "type": "string", "sensitive": false },
|
|
"session_lifetime": { "type": "integer", "sensitive": false },
|
|
"queue_driver": { "type": "string", "sensitive": false },
|
|
"log_driver": { "type": "string", "sensitive": false },
|
|
"log_level": { "type": "string", "sensitive": false },
|
|
"log_days": { "type": "integer", "sensitive": false },
|
|
"websocket_enabled": { "type": "boolean", "sensitive": false },
|
|
"websocket_app_id": { "type": "string", "sensitive": false },
|
|
"websocket_app_key": { "type": "string", "sensitive": false },
|
|
"websocket_app_secret": { "type": "string", "sensitive": true },
|
|
"websocket_host": { "type": "string", "sensitive": false },
|
|
"websocket_port": { "type": "integer", "sensitive": false },
|
|
"websocket_scheme": { "type": "string", "sensitive": false },
|
|
"websocket_verify_ssl": { "type": "boolean", "sensitive": false },
|
|
"websocket_server_host": { "type": "string", "sensitive": false },
|
|
"websocket_server_port": { "type": "integer", "sensitive": false },
|
|
"websocket_server_scheme": { "type": "string", "sensitive": false }
|
|
}
|
|
},
|
|
"notifications": {
|
|
"expose": true,
|
|
"fields": {
|
|
"channels": { "type": "array", "sensitive": false }
|
|
}
|
|
},
|
|
"identity": {
|
|
"expose": true,
|
|
"_comment": "본인인증 인프라 provider 기술 파라미터 — signup/enabled 등 정책 분기 키는 IdentityPolicy 로 흡수됨",
|
|
"fields": {
|
|
"default_provider": { "type": "string", "sensitive": false },
|
|
"purpose_providers": { "type": "array", "sensitive": false, "expose": false },
|
|
"challenge_ttl_minutes": { "type": "integer", "sensitive": false, "expose": false },
|
|
"max_attempts": { "type": "integer", "sensitive": false, "expose": false }
|
|
}
|
|
}
|
|
}
|
|
}
|