v7.0.0-beta.4 release

This commit is contained in:
HeuJung
2026-05-11 11:29:41 +09:00
parent 05db17887d
commit 1db039ff34
1545 changed files with 136783 additions and 11634 deletions
+52 -13
View File
@@ -163,6 +163,26 @@ return [
'en' => 'English',
],
/*
|--------------------------------------------------------------------------
| Locale → Country Code 휴리스틱 매핑
|--------------------------------------------------------------------------
|
| Accept-Language 헤더에서 국가 코드를 추출할 수 없을 때 사용하는
| 언어 코드 → 기본 국가 코드 폴백 매핑입니다. AuthService 가 사용합니다.
|
*/
'locale_country_fallback' => [
'ko' => 'KR',
'en' => 'US',
'ja' => 'JP',
'zh' => 'CN',
'de' => 'DE',
'fr' => 'FR',
'es' => 'ES',
],
/*
|--------------------------------------------------------------------------
| Encryption Key
@@ -211,7 +231,7 @@ return [
|
*/
'version' => env('APP_VERSION', '7.0.0-beta.3'),
'version' => env('APP_VERSION', '7.0.0-beta.4'),
/*
|--------------------------------------------------------------------------
@@ -250,21 +270,38 @@ return [
'github_url' => env('G7_UPDATE_GITHUB_URL', 'https://github.com/gnuboard/g7'),
'github_token' => env('G7_UPDATE_GITHUB_TOKEN', ''),
'pending_path' => env('G7_UPDATE_PENDING_PATH') ?: storage_path('app/core_pending'),
'targets' => array_filter(array_map('trim', explode(',', env('G7_UPDATE_TARGETS', 'app,bootstrap,config,database,docs,lang,resources,routes,public,tests,upgrades,artisan,composer.json,composer.json.default,composer.lock,package.json,package-lock.json,vite.config.js,vite.config.core.js,vitest.config.ts,tsconfig.json,phpunit.xml,.editorconfig,.gitattributes,.gitignore,README.md,CHANGELOG.md,modules/_bundled,plugins/_bundled,templates/_bundled')))),
'targets' => array_filter(array_map('trim', explode(',', env('G7_UPDATE_TARGETS', 'app,bootstrap,config,database,docs,lang,lang-packs/_bundled,resources,routes,public,tests,upgrades,artisan,composer.json,composer.json.default,composer.lock,package.json,package-lock.json,vite.config.js,vite.config.core.js,vitest.config.ts,tsconfig.json,phpunit.xml,.editorconfig,.gitattributes,.gitignore,README.md,CHANGELOG.md,modules/_bundled,plugins/_bundled,templates/_bundled')))),
'excludes' => array_filter(array_map('trim', explode(',', env('G7_UPDATE_EXCLUDES', 'node_modules,.git,bootstrap/cache')))),
// applyUpdate 의 "신규 최상위 항목 자동 발견" 폴백이 절대 덮어쓰면 안 되는 경로 목록.
// 런타임 데이터(`storage`), 로컬 환경(`.env*`), 별도 파이프라인 산출물(`vendor`),
// 개발 도구 메타(`.git`, `.claude`, `.serena` 등) 를 보호한다.
// 부모 프로세스의 stale `targets` 가 신버전 신규 디렉토리를 인식하지 못해
// 자동 발견이 트리거될 때만 참조된다 (정상 경로의 targets 는 항상 처리됨).
'protected_paths' => array_filter(array_map('trim', explode(',', env('G7_UPDATE_PROTECTED_PATHS', '.env,.env.local,.env.production,.env.testing,.env.testing.example,storage,vendor,node_modules,.git,.github,.idea,.vscode,.serena,.claude,.mcp.json,.phpunit.result.cache,core_pending')))),
'backup_only' => array_filter(array_map('trim', explode(',', env('G7_UPDATE_BACKUP_ONLY', 'vendor')))),
'backup_extra' => ['storage/app/settings'],
// 업데이트 종료 시 base_path() 소유자/그룹 기준으로 소유권을 재귀 복원할 경로 목록.
// sudo 실행 시 composer·artisan 등 외부 프로세스가 root 로 생성한 파일을 원상 회복한다.
//
// 기본값은 인스톨러 `public/install/includes/config.php:REQUIRED_DIRECTORIES` 의
// SSoT 경로와 1:1 정렬. 상위 → 하위 순서로 나열하여 복원 루프가 디렉토리 단위
// chown 후 하위 경로별 개별 복원을 수행하게 한다.
// 기본값 = "sudo update 가 root 로 새 파일을 만들 가능성이 있는 영역" 한정.
// 인스톨러 SSoT (`public/install/includes/config.php:REQUIRED_DIRECTORIES`) 의
// `'storage' => true (재귀 검증)` 와는 의미가 다르다 — 인스톨러 SSoT 는 "운영자가
// 초기 설치 시 storage 트리 전체에 적절한 권한을 부여했는지 검증" 용도이고,
// 본 설정은 "sudo 후 root 가 만든 파일의 소유권을 원상 복원" 용도. 두 책임이 다르므로
// 정렬이 깨져도 무방하다.
//
// storage 트리에서 chown 대상은 PHP-FPM 쓰기 필수 + sudo update 가 root 로 만들 수
// 있는 경로만 한정한다 (`storage/logs`, `storage/framework/{cache,sessions,views}`,
// `storage/app/core_pending`). `storage/app/{modules,plugins,attachments,public,settings}`
// 같은 사용자 데이터/모듈 storage 영역은 PHP-FPM 시드/업로드 시점 owner 가 보존
// 되어야 한다 (chown 대상에서 제외). 단 settings *.json 처럼 모듈/플러그인 upgrade
// step 이 sudo 컨텍스트에서 만들 수 있는 파일은 `SettingsMigrator::writeJsonFile`
// 가 부모 디렉토리 owner/group 을 상속하여 별도 처리.
//
// 환경변수 `G7_UPDATE_RESTORE_OWNERSHIP` 로 공유 호스팅 등 축소 필요 시 재정의 가능.
'restore_ownership' => array_filter(array_map('trim', explode(',', env(
'G7_UPDATE_RESTORE_OWNERSHIP',
'storage,bootstrap/cache,vendor,modules,modules/_pending,plugins,plugins/_pending,templates,templates/_pending,storage/app/core_pending'
'storage/logs,storage/framework,storage/app/core_pending,bootstrap/cache,vendor,modules,modules/_pending,plugins,plugins/_pending,templates,templates/_pending,lang-packs,lang-packs/_pending'
)))),
// 7.0.0-beta.3+: 그룹 쓰기 권한 비대칭 정상화 대상.
// sudo root 로 실행된 업데이트가 umask 022 로 신규 생성한 하위 디렉토리/파일이
@@ -275,22 +312,24 @@ return [
// 정책: 루트가 g+w 면 하위 g-w 항목을 g+w 로 승격, 다른 비트 무변경.
// 운영자가 의도적으로 그룹 쓰기를 차단한 경로(0755 등) 는 자동 보존됨.
//
// Laravel 런타임 그룹 쓰기 필요 경로 — 인스톨러 SSoT(public/install/includes/config.php
// REQUIRED_DIRECTORIES) 와 1:1 정렬:
// - storage, bootstrap/cache: 캐시·세션·로그
// Laravel 런타임 그룹 쓰기 필요 경로 — `restore_ownership` 과 동일한 책임 분리 원칙
// (PHP-FPM 쓰기 영역 한정, 사용자 데이터 영역 비대상). 인스톨러 SSoT 의 storage
// 재귀 검증 의도와는 다른 책임이다.
// - storage/logs, storage/framework/{cache,sessions,views}: 캐시·세션·로그
// - storage/app/core_pending: 코어 업데이트 _pending 영역
// - bootstrap/cache: Laravel 설정·라우트 캐시
// - vendor: composer/sudo 가 root 로 재생성한 후 일반 권한 사용자/php-fpm 이 후속 작업
// - modules, plugins, templates: 확장 설치/업데이트/제거 시 php-fpm 이 디렉토리 조작
// - modules/_pending, plugins/_pending, templates/_pending: 다운로드 대기소
// - storage/app/core_pending: 코어 업데이트 _pending 영역 (storage 재귀로도 커버되지만
// SSoT 정렬 위해 명시)
// - lang-packs, lang-packs/_pending: 언어팩 활성/대기 영역
//
// _bundled/ 는 개발 시점 원본 배포본이므로 런타임 쓰기 불필요 — SSoT 에도 미포함.
// _bundled/ 는 개발 시점 원본 배포본이므로 런타임 쓰기 불필요 — 미포함.
//
// 자식 디렉토리(예: plugins/sirsoft-*)는 syncGroupWritability 가 재귀 순회하여
// 자동 정상화되므로 상위 루트만 지정하면 충분. 환경변수로 재정의 가능.
'restore_ownership_group_writable' => array_filter(array_map('trim', explode(',', env(
'G7_UPDATE_RESTORE_OWNERSHIP_GROUP_WRITABLE',
'storage,bootstrap/cache,vendor,modules,modules/_pending,plugins,plugins/_pending,templates,templates/_pending,storage/app/core_pending'
'storage/logs,storage/framework,storage/app/core_pending,bootstrap/cache,vendor,modules,modules/_pending,plugins,plugins/_pending,templates,templates/_pending,lang-packs,lang-packs/_pending'
)))),
],
+544 -13
View File
@@ -145,6 +145,35 @@ return [
['identifier' => 'core.user-notifications.delete', 'type' => 'user', 'name' => ['ko' => '알림 삭제', 'en' => 'Delete Notifications'], 'description' => ['ko' => '본인의 알림을 삭제할 수 있습니다.', 'en' => 'Can delete own notifications.'], 'order' => 3],
],
],
[
'identifier' => 'core.identity',
'name' => ['ko' => '본인인증 (사용자)', 'en' => 'Identity Verification (User)'],
'description' => ['ko' => '로그인 사용자의 본인인증 challenge 요청/검증/취소 권한', 'en' => 'Permissions for authenticated users to request/verify/cancel IDV challenges'],
'category' => 'identity',
'order' => 7.7,
'type' => 'user',
'permissions' => [
['identifier' => 'core.identity.request', 'type' => 'user', 'name' => ['ko' => 'IDV 요청', 'en' => 'Request IDV Challenge'], 'description' => ['ko' => '본인인증 challenge 를 요청할 수 있습니다.', 'en' => 'Can request identity verification challenges.'], 'order' => 1],
['identifier' => 'core.identity.verify', 'type' => 'user', 'name' => ['ko' => 'IDV 검증', 'en' => 'Verify IDV Challenge'], 'description' => ['ko' => '본인의 본인인증 challenge 를 검증할 수 있습니다.', 'en' => 'Can verify own identity verification challenges.'], 'order' => 2, 'resource_route_key' => 'challenge', 'owner_key' => 'user_id'],
['identifier' => 'core.identity.cancel', 'type' => 'user', 'name' => ['ko' => 'IDV 취소', 'en' => 'Cancel IDV Challenge'], 'description' => ['ko' => '본인의 본인인증 challenge 를 취소할 수 있습니다.', 'en' => 'Can cancel own identity verification challenges.'], 'order' => 3, 'resource_route_key' => 'challenge', 'owner_key' => 'user_id'],
],
],
[
'identifier' => 'core.admin.identity',
'name' => ['ko' => '본인인증 관리 (관리자)', 'en' => 'Identity Verification Management (Admin)'],
'description' => ['ko' => '관리자용 IDV 프로바이더/정책/로그 관리 권한', 'en' => 'Admin permissions for IDV providers, policies, and logs'],
'category' => 'identity',
'order' => 7.8,
'type' => 'admin',
'permissions' => [
['identifier' => 'core.admin.identity.manage', 'type' => 'admin', 'name' => ['ko' => '프로바이더 설정', 'en' => 'Manage IDV Providers'], 'description' => ['ko' => '본인인증 프로바이더 설정을 관리할 수 있습니다.', 'en' => 'Can manage identity verification providers.'], 'order' => 1],
['identifier' => 'core.admin.identity.policies.manage', 'type' => 'admin', 'name' => ['ko' => '정책 관리', 'en' => 'Manage IDV Policies'], 'description' => ['ko' => '본인인증 정책(라우트/훅별)을 CRUD 할 수 있습니다.', 'en' => 'Can CRUD identity verification policies by route/hook.'], 'order' => 2],
['identifier' => 'core.admin.identity.logs.read', 'type' => 'admin', 'name' => ['ko' => '로그 열람', 'en' => 'View IDV Logs'], 'description' => ['ko' => '본인인증 이력을 조회할 수 있습니다.', 'en' => 'Can view identity verification logs.'], 'order' => 3],
['identifier' => 'core.admin.identity.logs.purge', 'type' => 'admin', 'name' => ['ko' => '로그 파기', 'en' => 'Purge IDV Logs'], 'description' => ['ko' => '본인인증 이력을 파기(보관주기 외)할 수 있습니다.', 'en' => 'Can purge identity verification logs (retention-based).'], 'order' => 4],
['identifier' => 'core.admin.identity.messages.read', 'type' => 'admin', 'name' => ['ko' => '메시지 템플릿 조회', 'en' => 'View IDV Message Templates'], 'description' => ['ko' => '본인인증 메시지 정의/템플릿을 조회할 수 있습니다.', 'en' => 'Can view identity verification message definitions and templates.'], 'order' => 5],
['identifier' => 'core.admin.identity.messages.update', 'type' => 'admin', 'name' => ['ko' => '메시지 템플릿 수정', 'en' => 'Update IDV Message Templates'], 'description' => ['ko' => '본인인증 메시지 정의/템플릿을 수정할 수 있습니다.', 'en' => 'Can update identity verification message definitions and templates.'], 'order' => 6],
],
],
[
'identifier' => 'core.settings',
'name' => ['ko' => '환경설정', 'en' => 'Settings'],
@@ -212,6 +241,20 @@ return [
['identifier' => 'core.schedules.run', 'type' => 'admin', 'name' => ['ko' => '스케줄 실행', 'en' => 'Run Schedules'], 'description' => ['ko' => '스케줄을 수동으로 실행할 수 있습니다.', 'en' => 'Can manually run schedules.'], 'order' => 5],
],
],
[
'identifier' => 'core.language_packs',
'name' => ['ko' => '언어팩 관리', 'en' => 'Language Pack Management'],
'description' => ['ko' => '언어팩 설치/제거/활성화 권한', 'en' => 'Language pack install/uninstall/activation permissions'],
'category' => 'language_packs',
'order' => 13,
'type' => 'admin',
'permissions' => [
['identifier' => 'core.language_packs.read', 'type' => 'admin', 'name' => ['ko' => '언어팩 조회', 'en' => 'View Language Packs'], 'description' => ['ko' => '설치된 언어팩 목록 및 상세 정보를 조회할 수 있습니다.', 'en' => 'Can view installed language pack list and details.'], 'order' => 1],
['identifier' => 'core.language_packs.install', 'type' => 'admin', 'name' => ['ko' => '언어팩 설치', 'en' => 'Install Language Packs'], 'description' => ['ko' => 'ZIP/GitHub/URL 로 언어팩을 설치할 수 있습니다.', 'en' => 'Can install language packs from ZIP/GitHub/URL.'], 'order' => 2],
['identifier' => 'core.language_packs.manage', 'type' => 'admin', 'name' => ['ko' => '언어팩 관리', 'en' => 'Manage Language Packs'], 'description' => ['ko' => '언어팩을 활성화/비활성화/제거할 수 있습니다.', 'en' => 'Can activate/deactivate/uninstall language packs.'], 'order' => 3],
['identifier' => 'core.language_packs.update', 'type' => 'admin', 'name' => ['ko' => '언어팩 업데이트', 'en' => 'Update Language Packs'], 'description' => ['ko' => '설치된 언어팩의 업데이트를 확인하고 실행할 수 있습니다.', 'en' => 'Can check and apply updates for installed language packs.'], 'order' => 4],
],
],
],
],
@@ -261,6 +304,12 @@ return [
'attributes' => ['is_active' => true],
'permissions' => [
'core.user-notifications.read', 'core.user-notifications.update', 'core.user-notifications.delete',
'core.identity.request', 'core.identity.verify', 'core.identity.cancel',
],
// verify/cancel 은 본인 challenge(user_id 일치) 만 — PermissionMiddleware 의 scope=self 가드가 자동 검증
'permission_scopes' => [
'core.identity.verify' => 'self',
'core.identity.cancel' => 'self',
],
],
[
@@ -271,7 +320,10 @@ return [
'en' => 'Unauthenticated user role. Permissions can be granted by administrators.',
],
'attributes' => ['is_active' => true],
'permissions' => [],
// 비로그인 가입(Mode B) · 비밀번호 재설정 등 IDV 진입에 필요 — PermissionMiddleware 가 비인증 시 scope 검사를 자동 스킵하므로 owner 가드는 throttle + UUID 추정 어려움에 의존
'permissions' => [
'core.identity.request', 'core.identity.verify', 'core.identity.cancel',
],
],
],
@@ -309,13 +361,22 @@ return [
'order' => 3,
'is_active' => true,
],
[
'slug' => 'admin-identity-logs',
'name' => ['ko' => '본인인증 이력', 'en' => 'Identity Logs'],
'url' => '/admin/identity/logs',
'icon' => 'fas fa-clipboard-check',
'parent_id' => null,
'order' => 4,
'is_active' => true,
],
[
'slug' => 'admin-activity-logs',
'name' => ['ko' => '활동 로그', 'en' => 'Activity Logs'],
'url' => '/admin/activity-logs',
'icon' => 'fas fa-history',
'parent_id' => null,
'order' => 4,
'order' => 5,
'is_active' => true,
],
[
@@ -324,7 +385,7 @@ return [
'url' => '/admin/menus',
'icon' => 'fas fa-bars',
'parent_id' => null,
'order' => 5,
'order' => 6,
'is_active' => true,
],
[
@@ -333,7 +394,7 @@ return [
'url' => '/admin/users',
'icon' => 'fas fa-users',
'parent_id' => null,
'order' => 6,
'order' => 7,
'is_active' => true,
],
[
@@ -342,7 +403,7 @@ return [
'url' => '/admin/roles',
'icon' => 'fas fa-lock',
'parent_id' => null,
'order' => 7,
'order' => 8,
'is_active' => true,
],
[
@@ -351,7 +412,7 @@ return [
'url' => '/admin/modules',
'icon' => 'fas fa-cube',
'parent_id' => null,
'order' => 8,
'order' => 9,
'is_active' => true,
],
[
@@ -360,7 +421,7 @@ return [
'url' => '/admin/plugins',
'icon' => 'fas fa-puzzle-piece',
'parent_id' => null,
'order' => 9,
'order' => 10,
'is_active' => true,
],
[
@@ -369,7 +430,7 @@ return [
'url' => '/admin/templates',
'icon' => 'fas fa-palette',
'parent_id' => null,
'order' => 10,
'order' => 11,
'is_active' => true,
],
[
@@ -378,7 +439,7 @@ return [
'url' => '/admin/schedules',
'icon' => 'fas fa-clock',
'parent_id' => null,
'order' => 10,
'order' => 12,
'is_active' => false,
],
],
@@ -387,24 +448,494 @@ return [
|--------------------------------------------------------------------------
| 코어 알림 정의
|--------------------------------------------------------------------------
| NotificationDefinitionSeeder에서 사용됩니다.
| 코어 알림 타입: welcome, reset_password, password_changed
| NotificationDefinitionSeeder 와 NotificationTemplateService 가 SSoT 로 사용합니다.
| NotificationSyncHelper 가 upsert 하며, 운영자가 관리자 UI 에서 수정한 필드는
| user_overrides JSON 에 보존되어 재시딩 시 덮어써지지 않습니다.
| 모듈/플러그인 자체 알림은 각자의 AbstractModule/AbstractPlugin::getNotificationDefinitions()
| 에서 선언하며 ModuleManager/PluginManager 가 자동 동기화합니다.
*/
'notification_definitions' => [
'welcome' => [
'hook_prefix' => 'core.auth',
'name' => ['ko' => '회원가입 환영', 'en' => 'Welcome'],
'description' => ['ko' => '회원가입 완료 시 발송되는 환영 알림', 'en' => 'Welcome notification sent upon registration'],
'channels' => ['mail', 'database'],
'hooks' => ['core.auth.after_register'],
'channels' => ['mail'],
'variables' => [
['key' => 'name', 'description' => '수신자 이름'],
['key' => 'app_name', 'description' => '사이트명'],
['key' => 'action_url', 'description' => '로그인 페이지 URL'],
['key' => 'site_url', 'description' => '사이트 URL'],
],
'templates' => [
[
'channel' => 'mail',
'recipients' => [['type' => 'trigger_user']],
'subject' => [
'ko' => '[{app_name}] 회원가입을 환영합니다',
'en' => '[{app_name}] Welcome to Our Service',
],
'body' => [
'ko' => '<h1>{name}님, 환영합니다!</h1>'
.'<p>{app_name}에 가입해 주셔서 감사합니다.</p>'
.'<p>이제 모든 서비스를 이용하실 수 있습니다. 아래 버튼을 클릭하여 로그인해 주세요.</p>'
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">로그인하기</a></td></tr></table>'
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
'en' => '<h1>Welcome, {name}!</h1>'
.'<p>Thank you for joining {app_name}.</p>'
.'<p>You now have access to all our services. Click the button below to log in.</p>'
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">Log In</a></td></tr></table>'
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
],
],
[
'channel' => 'database',
'recipients' => [['type' => 'trigger_user']],
'subject' => ['ko' => '회원가입을 환영합니다', 'en' => 'Welcome to our service'],
'body' => ['ko' => '{name}님, {app_name}에 가입해 주셔서 감사합니다.', 'en' => 'Welcome {name}, thank you for joining {app_name}.'],
'click_url' => '/mypage',
],
],
],
'reset_password' => [
'hook_prefix' => 'core.auth',
'name' => ['ko' => '비밀번호 재설정', 'en' => 'Password Reset'],
'description' => ['ko' => '비밀번호 재설정 요청 시 발송되는 알림', 'en' => 'Notification sent when password reset is requested'],
'channels' => ['mail', 'database'],
'hooks' => ['core.auth.after_reset_password_request'],
'channels' => ['mail'],
'variables' => [
['key' => 'name', 'description' => '수신자 이름'],
['key' => 'app_name', 'description' => '사이트명'],
['key' => 'action_url', 'description' => '비밀번호 재설정 URL'],
['key' => 'expire_minutes', 'description' => '링크 만료 시간(분)'],
['key' => 'site_url', 'description' => '사이트 URL'],
],
'templates' => [
[
'channel' => 'mail',
'recipients' => [['type' => 'trigger_user']],
'subject' => [
'ko' => '[{app_name}] 비밀번호 재설정 안내',
'en' => '[{app_name}] Password Reset Request',
],
'body' => [
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
.'<p>비밀번호 재설정 요청을 받았습니다. 아래 버튼을 클릭하여 비밀번호를 재설정해 주세요.</p>'
.'<p>이 링크는 <strong>{expire_minutes}분</strong> 후 만료됩니다.</p>'
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">비밀번호 재설정</a></td></tr></table>'
.'<p>비밀번호 재설정을 요청하지 않으셨다면, 이 이메일을 무시해 주세요.</p>'
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
'en' => '<h1>Hello, {name}.</h1>'
.'<p>We received a request to reset your password. Click the button below to set a new password.</p>'
.'<p>This link will expire in <strong>{expire_minutes} minutes</strong>.</p>'
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">Reset Password</a></td></tr></table>'
.'<p>If you did not request a password reset, please ignore this email.</p>'
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
],
],
[
'channel' => 'database',
'recipients' => [['type' => 'trigger_user']],
'subject' => ['ko' => '비밀번호 재설정 안내', 'en' => 'Password Reset Request'],
'body' => ['ko' => '{name}님, 비밀번호 재설정이 요청되었습니다. 본인이 요청하지 않았다면 이 알림을 무시해 주세요.', 'en' => '{name}, a password reset has been requested. If you did not request this, please ignore this notification.'],
'click_url' => '{action_url}',
],
],
],
'password_changed' => [
'hook_prefix' => 'core.auth',
'name' => ['ko' => '비밀번호 변경', 'en' => 'Password Changed'],
'description' => ['ko' => '비밀번호 변경 완료 시 발송되는 알림', 'en' => 'Notification sent when password is changed'],
'channels' => ['mail', 'database'],
'hooks' => ['core.auth.after_password_changed'],
'variables' => [
['key' => 'name', 'description' => '수신자 이름'],
['key' => 'app_name', 'description' => '사이트명'],
['key' => 'action_url', 'description' => '로그인 페이지 URL'],
['key' => 'site_url', 'description' => '사이트 URL'],
],
'templates' => [
[
'channel' => 'mail',
'recipients' => [['type' => 'trigger_user']],
'subject' => [
'ko' => '[{app_name}] 비밀번호가 변경되었습니다',
'en' => '[{app_name}] Your Password Has Been Changed',
],
'body' => [
'ko' => '<h1>{name}님, 안녕하세요.</h1>'
.'<p>계정의 비밀번호가 성공적으로 변경되었습니다.</p>'
.'<p><strong>본인이 변경하지 않았다면, 즉시 고객 지원팀에 문의하시기 바랍니다.</strong></p>'
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">로그인하기</a></td></tr></table>'
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
'en' => '<h1>Hello, {name}.</h1>'
.'<p>Your account password has been successfully changed.</p>'
.'<p><strong>If you did not make this change, please contact our support team immediately.</strong></p>'
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 24px 0;"><tr><td align="center"><a href="{action_url}" style="display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;">Log In</a></td></tr></table>'
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
],
],
[
'channel' => 'database',
'recipients' => [['type' => 'trigger_user']],
'subject' => ['ko' => '비밀번호가 변경되었습니다', 'en' => 'Your password has been changed'],
'body' => ['ko' => '{name}님, 비밀번호가 변경되었습니다. 본인이 변경하지 않았다면 즉시 고객 지원팀에 문의하시기 바랍니다.', 'en' => '{name}, your password has been changed. If you did not make this change, please contact support immediately.'],
'click_url' => '/mypage/change-password',
],
],
],
],
/*
|--------------------------------------------------------------------------
| 코어 본인인증(IDV) 정책
|--------------------------------------------------------------------------
| IdentityPolicySeeder 에서 사용됩니다. IdentityPolicySyncHelper 가 upsert 하며,
| 운영자가 S1d UI 에서 수정한 필드는 user_overrides JSON 에 기록되어 재시딩 시 보존됩니다.
*/
'identity_policies' => [
// 회원가입 — Mode B (route 단계, verification_token 검증 필수)
'core.auth.signup_before_submit' => [
'scope' => 'route',
'target' => 'api.auth.register',
'purpose' => 'signup',
'provider_id' => null,
'grace_minutes' => 0,
'enabled' => false,
'applies_to' => 'self',
'fail_mode' => 'block',
'priority' => 110,
'conditions' => ['signup_stage' => 'before_submit', 'http_method' => ['POST']],
],
// 회원가입 — Mode C (hook 단계, after_register 시 challenge 발행 + PendingVerification)
'core.auth.signup_after_create' => [
'scope' => 'hook',
'target' => 'core.auth.after_register',
'purpose' => 'signup',
'provider_id' => null,
'grace_minutes' => 0,
'enabled' => false,
'applies_to' => 'self',
'fail_mode' => 'block',
'priority' => 100,
'conditions' => ['signup_stage' => 'after_create'],
],
// 비밀번호 재설정 — 기존 forgotPassword 흐름이 IDV 인프라 경유 (안전 기본 OFF)
'core.auth.password_reset' => [
'scope' => 'hook',
'target' => 'core.auth.before_reset_password',
'purpose' => 'password_reset',
'provider_id' => null,
'grace_minutes' => 0,
'enabled' => false,
'applies_to' => 'both',
'fail_mode' => 'block',
'priority' => 100,
],
// 비밀번호 변경 (로그인 상태 자기 변경)
'core.profile.password_change' => [
'scope' => 'route',
'target' => 'api.me.password',
'purpose' => 'sensitive_action',
'grace_minutes' => 5,
'enabled' => true,
'applies_to' => 'self',
'fail_mode' => 'block',
],
// 민감 정보 변경 (이메일/전화 변경)
'core.profile.contact_change' => [
'scope' => 'hook',
'target' => 'core.user.before_update',
'purpose' => 'sensitive_action',
'grace_minutes' => 5,
'enabled' => true,
'applies_to' => 'self',
'fail_mode' => 'block',
'conditions' => ['changed_fields' => ['email', 'phone', 'mobile']],
],
// 계정 탈퇴
'core.account.withdraw' => [
'scope' => 'route',
'target' => 'api.me.destroy',
'purpose' => 'sensitive_action',
'grace_minutes' => 0,
'enabled' => true,
'applies_to' => 'self',
'fail_mode' => 'block',
],
// 관리자: App Key 재생성
'core.admin.app_key_regenerate' => [
'scope' => 'route',
'target' => 'api.admin.settings.regenerate-app-key',
'purpose' => 'sensitive_action',
'grace_minutes' => 0,
'enabled' => false,
'applies_to' => 'admin',
'fail_mode' => 'block',
],
// 관리자: 사용자 삭제
'core.admin.user_delete' => [
'scope' => 'hook',
'target' => 'core.user.before_delete',
'purpose' => 'sensitive_action',
'grace_minutes' => 0,
'enabled' => false,
'applies_to' => 'admin',
'fail_mode' => 'block',
],
// 관리자: 모듈/플러그인 제거
'core.admin.extension_uninstall' => [
'scope' => 'route',
'target' => 'api.admin.{modules,plugins}.uninstall',
'purpose' => 'sensitive_action',
'grace_minutes' => 0,
'enabled' => false,
'applies_to' => 'admin',
'fail_mode' => 'block',
],
],
/*
|--------------------------------------------------------------------------
| 코어 본인인증(IDV) 메시지 정의/템플릿
|--------------------------------------------------------------------------
| IdentityMessageDefinitionSeeder 에서 사용됩니다. IdentityMessageSyncHelper 가
| upsert 하며, 운영자가 관리자 UI 에서 수정한 필드는 user_overrides JSON 에 보존됩니다.
| 모듈/플러그인 자체 IDV 메시지는 각자의 getIdentityMessages() 에서 선언합니다.
|
| 각 항목 키는 사람이 읽기 쉬운 식별자이며, 실제 unique 키는 (provider_id, scope_type, scope_value) 조합입니다.
| scope_type: 'provider_default' | 'purpose' | 'policy'
|
| 코어 변수(commonVariables) 는 모든 mail 정의 공통:
| code, action_url, expire_minutes, purpose_label, app_name, site_url, recipient_email
*/
'identity_messages' => [
'mail.provider_default' => [
'provider_id' => 'g7:core.mail',
'scope_type' => 'provider_default',
'scope_value' => '',
'name' => ['ko' => '메일 본인 확인 (기본)', 'en' => 'Mail Verification (default)'],
'description' => [
'ko' => '특정 목적이 매칭되지 않을 때 사용되는 기본 메일 템플릿',
'en' => 'Fallback mail template used when no specific purpose matches',
],
'channels' => ['mail'],
'variables' => '__common__',
'templates' => [
[
'channel' => 'mail',
'subject' => [
'ko' => '[{app_name}] 본인 확인 인증 코드',
'en' => '[{app_name}] Verification Code',
],
'body' => [
'ko' => '<p>안녕하세요.</p>'
.'<p>아래 인증 코드를 입력해 본인 확인을 완료해 주세요.</p>'
.'<p style="font-size:24px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;">{code}</p>'
.'<p>이 코드는 <strong>{expire_minutes}분</strong> 후 만료됩니다.</p>'
.'<p>본인이 요청하지 않았다면 이 메일을 무시해 주세요.</p>'
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
'en' => '<p>Hello.</p>'
.'<p>Please enter the verification code below to confirm your identity.</p>'
.'<p style="font-size:24px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;">{code}</p>'
.'<p>This code will expire in <strong>{expire_minutes} minutes</strong>.</p>'
.'<p>If you did not request this, please ignore this email.</p>'
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
],
],
],
],
'mail.purpose.signup' => [
'provider_id' => 'g7:core.mail',
'scope_type' => 'purpose',
'scope_value' => 'signup',
'name' => ['ko' => '회원가입 인증', 'en' => 'Signup Verification'],
'description' => [
'ko' => '회원가입 단계에서 이메일 본인 확인 시 발송',
'en' => 'Sent when verifying email during signup',
],
'channels' => ['mail'],
'variables' => '__common__',
'templates' => [
[
'channel' => 'mail',
'subject' => [
'ko' => '[{app_name}] 회원가입 인증 코드',
'en' => '[{app_name}] Signup Verification Code',
],
'body' => [
'ko' => '<h1>{app_name} 회원가입 인증</h1>'
.'<p>아래 인증 코드를 회원가입 화면에 입력해 주세요.</p>'
.'<p style="font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;">{code}</p>'
.'<p>이 코드는 <strong>{expire_minutes}분</strong> 후 만료됩니다.</p>'
.'<p>본인이 가입을 시도하지 않았다면 이 메일을 무시해 주세요.</p>'
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
'en' => '<h1>{app_name} Signup Verification</h1>'
.'<p>Please enter the verification code below on the signup screen.</p>'
.'<p style="font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;">{code}</p>'
.'<p>This code will expire in <strong>{expire_minutes} minutes</strong>.</p>'
.'<p>If you did not request this, please ignore this email.</p>'
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
],
],
],
],
'mail.purpose.password_reset' => [
'provider_id' => 'g7:core.mail',
'scope_type' => 'purpose',
'scope_value' => 'password_reset',
'name' => ['ko' => '비밀번호 재설정 링크', 'en' => 'Password Reset Link'],
'description' => [
'ko' => '비밀번호 재설정 요청 시 발송되는 서명 링크 메일 (link 흐름)',
'en' => 'Signed-link email sent on password reset request (link flow)',
],
'channels' => ['mail'],
'variables' => '__common__',
'templates' => [
[
'channel' => 'mail',
'subject' => [
'ko' => '[{app_name}] 비밀번호 재설정 안내',
'en' => '[{app_name}] Password Reset Request',
],
'body' => [
'ko' => '<h1>비밀번호 재설정</h1>'
.'<p>비밀번호 재설정 요청을 받았습니다. 아래 버튼을 클릭하여 비밀번호를 재설정해 주세요.</p>'
.'<p>이 링크는 <strong>{expire_minutes}분</strong> 후 만료됩니다.</p>'
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:24px 0;"><tr><td align="center"><a href="{action_url}" style="display:inline-block; padding:12px 32px; background-color:#2d3748; color:#ffffff; text-decoration:none; border-radius:4px; font-weight:600; font-size:14px;">비밀번호 재설정</a></td></tr></table>'
.'<p>본인이 요청하지 않았다면 이 메일을 무시해 주세요.</p>'
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
'en' => '<h1>Password Reset</h1>'
.'<p>We received a password reset request. Click the button below to set a new password.</p>'
.'<p>This link will expire in <strong>{expire_minutes} minutes</strong>.</p>'
.'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:24px 0;"><tr><td align="center"><a href="{action_url}" style="display:inline-block; padding:12px 32px; background-color:#2d3748; color:#ffffff; text-decoration:none; border-radius:4px; font-weight:600; font-size:14px;">Reset Password</a></td></tr></table>'
.'<p>If you did not request this, please ignore this email.</p>'
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
],
],
],
],
'mail.purpose.self_update' => [
'provider_id' => 'g7:core.mail',
'scope_type' => 'purpose',
'scope_value' => 'self_update',
'name' => ['ko' => '계정 정보 변경 인증', 'en' => 'Self-update Verification'],
'description' => [
'ko' => '계정 정보 변경 시 본인 확인 코드 메일',
'en' => 'Verification code mail when updating account info',
],
'channels' => ['mail'],
'variables' => '__common__',
'templates' => [
[
'channel' => 'mail',
'subject' => [
'ko' => '[{app_name}] 계정 정보 변경 인증 코드',
'en' => '[{app_name}] Account Update Verification Code',
],
'body' => [
'ko' => '<h1>계정 정보 변경 인증</h1>'
.'<p>{purpose_label} 처리를 위해 아래 인증 코드를 입력해 주세요.</p>'
.'<p style="font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;">{code}</p>'
.'<p>이 코드는 <strong>{expire_minutes}분</strong> 후 만료됩니다.</p>'
.'<p><strong>본인이 변경을 시도하지 않았다면 즉시 비밀번호를 변경하시기 바랍니다.</strong></p>'
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
'en' => '<h1>Account Update Verification</h1>'
.'<p>Please enter the code below to proceed with {purpose_label}.</p>'
.'<p style="font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;">{code}</p>'
.'<p>This code will expire in <strong>{expire_minutes} minutes</strong>.</p>'
.'<p><strong>If you did not initiate this change, please change your password immediately.</strong></p>'
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
],
],
],
],
'mail.purpose.sensitive_action' => [
'provider_id' => 'g7:core.mail',
'scope_type' => 'purpose',
'scope_value' => 'sensitive_action',
'name' => ['ko' => '중요 작업 인증', 'en' => 'Sensitive Action Verification'],
'description' => [
'ko' => '결제, 주요 설정 변경 등 중요 작업 시 본인 확인 코드 메일',
'en' => 'Verification code mail for sensitive actions (payment, critical settings)',
],
'channels' => ['mail'],
'variables' => '__common__',
'templates' => [
[
'channel' => 'mail',
'subject' => [
'ko' => '[{app_name}] 중요 작업 인증 코드',
'en' => '[{app_name}] Sensitive Action Verification Code',
],
'body' => [
'ko' => '<h1>중요 작업 인증</h1>'
.'<p>{purpose_label} 진행을 위해 아래 인증 코드를 입력해 주세요.</p>'
.'<p style="font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;">{code}</p>'
.'<p>이 코드는 <strong>{expire_minutes}분</strong> 후 만료됩니다.</p>'
.'<p><strong>본인이 시도하지 않았다면 즉시 고객 지원팀에 문의해 주세요.</strong></p>'
.'<p>감사합니다,<br><a href="{site_url}">{app_name}</a></p>',
'en' => '<h1>Sensitive Action Verification</h1>'
.'<p>Please enter the code below to proceed with {purpose_label}.</p>'
.'<p style="font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;">{code}</p>'
.'<p>This code will expire in <strong>{expire_minutes} minutes</strong>.</p>'
.'<p><strong>If you did not initiate this action, please contact support immediately.</strong></p>'
.'<p>Thank you,<br><a href="{site_url}">{app_name}</a></p>',
],
],
],
],
],
/*
|--------------------------------------------------------------------------
| 코어 본인인증(IDV) 목적 (purpose) 메타데이터
|--------------------------------------------------------------------------
| App\Enums\IdentityVerificationPurpose enum 이 코드 계약(타입 안전) 을 담당하고,
| 본 블록은 라벨/설명/기본 provider/허용 채널 등 운영 메타데이터의 SSoT 입니다.
| IdentityVerificationManager::registerCorePurposes() 가 부팅 시 본 블록을 로드하며,
| 모듈/플러그인은 각자의 getIdentityPurposes() 에서 추가 purpose 를 선언합니다.
| 라벨은 lang/{locale}/identity.php::purposes.{key}.label 가 우선 (다국어 정합).
*/
'identity_purposes' => [
'signup' => [
// 'label' / 'description' 의 값은 i18n 키 문자열. resolvePurposeText 가 __() 로 풀이.
// (`label_key` / `description_key` 명명은 controller 쪽이 'label'/'description' 만
// 인식해 동작하지 않으므로 표준 명명으로 정렬)
'label' => 'identity.purposes.signup.label',
'description' => 'identity.purposes.signup.description',
'default_provider' => 'g7:core.mail',
'allowed_channels' => ['mail', 'sms'],
],
'password_reset' => [
'label' => 'identity.purposes.password_reset.label',
'description' => 'identity.purposes.password_reset.description',
'default_provider' => 'g7:core.mail',
'allowed_channels' => ['mail', 'sms'],
],
'self_update' => [
'label' => 'identity.purposes.self_update.label',
'description' => 'identity.purposes.self_update.description',
'default_provider' => 'g7:core.mail',
'allowed_channels' => ['mail', 'sms'],
],
'sensitive_action' => [
'label' => 'identity.purposes.sensitive_action.label',
'description' => 'identity.purposes.sensitive_action.description',
'default_provider' => 'g7:core.mail',
'allowed_channels' => ['mail', 'sms'],
],
],
+8 -2
View File
@@ -73,7 +73,10 @@ return [
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
(PHP_VERSION_ID >= 80500 && class_exists('Pdo\\Mysql', false))
? \Pdo\Mysql::ATTR_SSL_CA
: PDO::MYSQL_ATTR_SSL_CA
=> env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
@@ -93,7 +96,10 @@ return [
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
(PHP_VERSION_ID >= 80500 && class_exists('Pdo\\Mysql', false))
? \Pdo\Mysql::ATTR_SSL_CA
: PDO::MYSQL_ATTR_SSL_CA
=> env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
+7 -6
View File
@@ -7,24 +7,25 @@ return [
| 기본 채널 설정
|--------------------------------------------------------------------------
| 시스템에서 기본으로 사용 가능한 알림 채널 목록입니다.
| 라벨은 lang key 로 선언되어 활성 언어팩(ko/en/ja 등) 으로 자동 보강됩니다.
| 플러그인에서 core.notification.filter_available_channels 훅으로 확장 가능합니다.
*/
'default_channels' => [
[
'id' => 'mail',
'name' => ['ko' => '메일', 'en' => 'Email'],
'name_key' => 'notification.channels.mail.name',
'icon' => 'fas fa-envelope',
'description' => ['ko' => '이메일로 알림 발송', 'en' => 'Send notification via email'],
'description_key' => 'notification.channels.mail.description',
'source' => 'core',
'source_label' => ['ko' => '코어 기본 채널', 'en' => 'Core default channel'],
'source_label_key' => 'notification.channels.core_default',
],
[
'id' => 'database',
'name' => ['ko' => '사이트내 알림', 'en' => 'Site Notification'],
'name_key' => 'notification.channels.database.name',
'icon' => 'fas fa-bell',
'description' => ['ko' => '사이트내 알림 센터에 표시', 'en' => 'Show in site notification center'],
'description_key' => 'notification.channels.database.description',
'source' => 'core',
'source_label' => ['ko' => '코어 기본 채널', 'en' => 'Core default channel'],
'source_label_key' => 'notification.channels.core_default',
],
],
+42 -4
View File
@@ -2,7 +2,7 @@
"_meta": {
"version": "1.0.0",
"description": "그누보드7 환경설정 기본값 및 프론트엔드 스키마",
"categories": ["general", "security", "mail", "upload", "seo", "cache", "debug", "drivers", "core_update", "geoip", "notifications"]
"categories": ["general", "security", "mail", "upload", "seo", "cache", "debug", "drivers", "core_update", "geoip", "notifications", "identity"]
},
"defaults": {
"general": {
@@ -56,14 +56,23 @@
"google_analytics_id": "",
"google_site_verification": "",
"naver_site_verification": "",
"bot_user_agents": ["Googlebot", "Bingbot", "Naverbot", "Yeti", "Yandex"],
"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": true,
"cache_ttl": 7200,
"sitemap_enabled": true,
"sitemap_cache_ttl": 86400,
"sitemap_schedule": "daily",
"sitemap_schedule_time": "02:00"
"sitemap_schedule_time": "02:00",
"sitemap_last_updated_at": "",
"generator_enabled": true,
"generator_content": ""
},
"cache": {
"enabled": true,
@@ -134,6 +143,16 @@
{ "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": {
@@ -183,12 +202,21 @@
"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_schedule": { "type": "string", "sensitive": false },
"sitemap_schedule_time": { "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": {
@@ -282,6 +310,16 @@
"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 }
}
}
}
}