v7.0.0-beta.4 release
This commit is contained in:
+52
-13
@@ -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
@@ -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
@@ -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,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',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user