feat(core,admin_basic): .env 키 단위 우선 모드 추가
관리자 환경설정 저장값이 config 를 덮는 기존 설계는 공유호스팅을 1차 대상으로 삼은 결과다. .env 를 배포 기준값으로 관리하는 설치에서는 그 설계가 반대로 작동해, 설치기가 defaults 를 시드하는 순간부터 .env 의 28개 항목이 오류도 로그도 없이 사문화된다. G7_ENV_PRIORITY 옵트인으로 그 소유권을 키 단위로 되돌린다. 스위치를 켜지 않은 설치는 전 경로가 조기 return 이라 동작이 100% 동일하다. 명시 여부는 config 빌드 시점에 캡처한다 — 런타임 env 는 config:cache 환경에서 null 로 고정되어 판정이 영구 미발동한다(attachment.disk_explicit 와 같은 함정). 판정은 strict 라 APP_DEBUG=false·REDIS_DB=0 같은 falsy 명시도 명시로 본다. 잠금은 그 키의 주입만 건너뛰는 것이지만, 제거된 키의 부재를 값으로 읽는 자리가 있으면 형제 설정까지 무너진다. 게이트(웹소켓 토글·메일 드라이버·디버그 모드)와 기본값 주입(mailgun.endpoint·ses.region), 형제 폴백(reverb server endpoint), 파생 판정(storage_driver→attachment.disk)을 유효값으로 보정했다.
This commit is contained in:
1 parent
b97deee43a
commit
381fcc4e0b
37 files changed
+4026
-118
No files matched your search
@@ -4,6 +4,7 @@ namespace App\Providers;
|
||||
|
||||
use App\Repositories\JsonConfigRepository;
|
||||
use App\Support\AllowedExtensions;
|
||||
use App\Support\EnvPriority;
|
||||
use App\Support\ExtensionSettingsMirror;
|
||||
use App\Support\OutboundProxy;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
@@ -82,6 +83,10 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
return;
|
||||
}
|
||||
|
||||
// .env 우선 모드에서 `.env` 가 소유권을 가져간 키를 제거한다 — 아래 가드들이
|
||||
// 제거된 키를 자연히 건너뛰므로 지점마다 조건을 심지 않는다 (스위치 OFF 면 무동작).
|
||||
$mailSettings = EnvPriority::filterLocked('mail', $mailSettings);
|
||||
|
||||
// 메일러 설정
|
||||
if (! empty($mailSettings['mailer'])) {
|
||||
Config::set('mail.default', $mailSettings['mailer']);
|
||||
@@ -108,8 +113,13 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
Config::set('mail.mailers.smtp.encryption', $mailSettings['encryption'] ?: null);
|
||||
}
|
||||
|
||||
// 드라이버별 설정
|
||||
$mailer = $mailSettings['mailer'] ?? '';
|
||||
// 드라이버별 설정 — 마스터 드라이버가 `.env` 로 잠기면 저장값이 이 배열에서 제거되어
|
||||
// 게이트가 빈 문자열이 된다. 그대로 두면 어느 분기에도 들어가지 않아 mailgun/ses 하위
|
||||
// 저장값(도메인·자격증명 등, 각자 잠기지 않았다)이 조용히 주입되지 않는다.
|
||||
// 잠긴 경우에는 유효값(= `.env` 유래 config)으로 게이트를 보정한다.
|
||||
$mailer = EnvPriority::isLocked('mail.mailer')
|
||||
? (string) config('mail.default')
|
||||
: ($mailSettings['mailer'] ?? '');
|
||||
|
||||
if ($mailer === 'mailgun') {
|
||||
if (! empty($mailSettings['mailgun_domain'])) {
|
||||
@@ -118,9 +128,15 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
if (! empty($mailSettings['mailgun_secret'])) {
|
||||
Config::set('services.mailgun.secret', $mailSettings['mailgun_secret']);
|
||||
}
|
||||
Config::set('services.mailgun.endpoint',
|
||||
! empty($mailSettings['mailgun_endpoint']) ? $mailSettings['mailgun_endpoint'] : 'api.mailgun.net'
|
||||
);
|
||||
// 저장값이 비어도 기본값을 박는다 — 종전 동작이다. 주입을 건너뛰는 경우는
|
||||
// `.env` 우선 모드가 이 키를 가져간 때뿐이므로 잠금 여부를 직접 묻는다.
|
||||
// 키 존재 여부(`array_key_exists`)로 판정하면 "저장값이 없는 호출"과 "잠겨서 제거된 호출"이
|
||||
// 구분되지 않아, 전자에서도 기본값 주입이 사라진다.
|
||||
if (! EnvPriority::isLocked('mail.mailgun_endpoint')) {
|
||||
Config::set('services.mailgun.endpoint',
|
||||
! empty($mailSettings['mailgun_endpoint']) ? $mailSettings['mailgun_endpoint'] : 'api.mailgun.net'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($mailer === 'ses') {
|
||||
@@ -130,9 +146,12 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
if (! empty($mailSettings['ses_secret'])) {
|
||||
Config::set('services.ses.secret', $mailSettings['ses_secret']);
|
||||
}
|
||||
Config::set('services.ses.region',
|
||||
! empty($mailSettings['ses_region']) ? $mailSettings['ses_region'] : 'ap-northeast-2'
|
||||
);
|
||||
// mailgun_endpoint 와 같은 사유의 가드 (위 주석 참조).
|
||||
if (! EnvPriority::isLocked('mail.ses_region')) {
|
||||
Config::set('services.ses.region',
|
||||
! empty($mailSettings['ses_region']) ? $mailSettings['ses_region'] : 'ap-northeast-2'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 발신자 설정
|
||||
@@ -156,6 +175,9 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
return;
|
||||
}
|
||||
|
||||
// .env 우선 모드: `.env` 가 소유한 키 제거 (스위치 OFF 면 무동작).
|
||||
$generalSettings = EnvPriority::filterLocked('general', $generalSettings);
|
||||
|
||||
if (! empty($generalSettings['site_name'])) {
|
||||
// site_name 이 다국어 JSON array 일 수 있으므로 현재/폴백 로케일 string 으로 정규화한다
|
||||
// (공개#49). raw array 를 config('app.name') 에 넣으면 app.blade.php 의
|
||||
@@ -240,6 +262,10 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
*/
|
||||
private function applyDebugConfig(JsonConfigRepository $configRepository): void
|
||||
{
|
||||
// G7_PLAYWRIGHT_BYPASS 는 호출자가 그 프로세스에만 부여하는 프로세스 환경변수다 —
|
||||
// `.env` 파일에 적히지 않으므로 config:cache 로 박제될 대상이 아니고, env() 가
|
||||
// 프로세스 환경을 그대로 읽으므로 config:cache 환경에서도 정상 판별된다.
|
||||
// (`.env` 유래 값의 런타임 env() 판별이 무력해지는 함정과는 다른 축이다.)
|
||||
if (app()->environment('testing') || env('G7_PLAYWRIGHT_BYPASS') === '1') {
|
||||
return;
|
||||
}
|
||||
@@ -250,14 +276,29 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
return;
|
||||
}
|
||||
|
||||
$isDebugMode = isset($debugSettings['mode']) && (bool) $debugSettings['mode'];
|
||||
// .env 우선 모드: `.env` 가 소유한 키 제거 (스위치 OFF 면 무동작).
|
||||
$debugSettings = EnvPriority::filterLocked('debug', $debugSettings);
|
||||
|
||||
$modeLocked = EnvPriority::isLocked('debug.mode');
|
||||
$logLevelLocked = EnvPriority::isLocked('debug.log_level');
|
||||
|
||||
// 디버그 모드가 `.env` 로 잠기면 그 유효값은 config('app.debug')(= APP_DEBUG)다.
|
||||
// 저장값을 읽으면 잠금으로 키가 사라져 항상 false 가 되고, 아래 두 2차 효과
|
||||
// (로그 레벨 강제·프록시 게이트)가 운영자 의도와 반대로 동작한다.
|
||||
$isDebugMode = $modeLocked
|
||||
? (bool) config('app.debug')
|
||||
: (isset($debugSettings['mode']) && (bool) $debugSettings['mode']);
|
||||
|
||||
if (isset($debugSettings['mode'])) {
|
||||
Config::set('app.debug', $isDebugMode);
|
||||
}
|
||||
|
||||
// debug 모드가 true이면 log_level을 debug로 강제 설정
|
||||
if ($isDebugMode) {
|
||||
// debug 모드가 true이면 log_level을 debug로 강제 설정.
|
||||
// 단 로그 레벨이 `.env` 로 잠긴 설치에서는 강제 자체를 하지 않는다 — 그 강제는
|
||||
// 저장값 경로의 편의 규칙이고, 잠긴 키의 권위는 `.env` 에 있다.
|
||||
if ($logLevelLocked) {
|
||||
$logLevel = null;
|
||||
} elseif ($isDebugMode) {
|
||||
$logLevel = 'debug';
|
||||
} elseif (! empty($debugSettings['log_level'])) {
|
||||
$logLevel = $debugSettings['log_level'];
|
||||
@@ -279,7 +320,10 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
// 아웃바운드 HTTP 프록시 설정.
|
||||
// 적용 여부 판정은 OutboundProxy 가 단독으로 소유한다 — 디버그 모드가 꺼져 있으면
|
||||
// 저장값이 남아 있어도 null 이 되어 주입되지 않는다.
|
||||
Config::set('g7.outbound_proxy', OutboundProxy::resolve($debugSettings));
|
||||
// 게이트 값(mode)은 위에서 구한 유효값으로 보정해 넘긴다 — `+` 는 존재하는 키를
|
||||
// 덮지 않으므로 잠기지 않은 설치에서는 종전과 동일하다. 보정이 없으면 디버그 모드가
|
||||
// `.env` 로 잠긴 설치에서 프록시가 영구 미적용된다 (오류·로그 없이).
|
||||
Config::set('g7.outbound_proxy', OutboundProxy::resolve($debugSettings + ['mode' => $isDebugMode]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,10 +345,17 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
return;
|
||||
}
|
||||
|
||||
// .env 우선 모드: `.env` 가 소유한 키 제거 (스위치 OFF 면 무동작).
|
||||
// 아래 apply*Config 들은 이 배열을 인자로 받으므로 redis/reverb 처럼 한 설정이
|
||||
// 여러 config 키를 파생시키는 경우도 한 번에 처리된다.
|
||||
$driverSettings = EnvPriority::filterLocked('drivers', $driverSettings);
|
||||
|
||||
// testing 환경에서는 drivers.json의 cache/session/queue 오버라이드 차단 — 테스트 격리 보호
|
||||
// (storage/app/settings/drivers.json은 shared 파일이므로 dev의 Redis/DB 드라이버가
|
||||
// testing으로 흘러들어가면 dev 캐시를 오염시킴)
|
||||
$isTestingEnv = env('APP_ENV') === 'testing';
|
||||
// env() 직접 호출은 config:cache 환경에서 null 로 고정되어 가드가 무력해지므로
|
||||
// 해석된 환경(app()->environment)으로 판정한다 — applyDebugConfig 와 동일 규약.
|
||||
$isTestingEnv = app()->environment('testing');
|
||||
|
||||
// 캐시 드라이버 설정
|
||||
if (! $isTestingEnv && ! empty($driverSettings['cache_driver'])) {
|
||||
@@ -346,7 +397,14 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
// 빈 문자열도 미명시로 취급한다 — `ATTACHMENT_DISK=` 가 복사된 .env 에서
|
||||
// 빈 값을 명시로 읽으면 전환이 영구 미발동한다 (config 정규화의 2차 방어).
|
||||
// 기존 행은 행 disk 로 서빙되므로 신구 디스크 혼재는 안전하다.
|
||||
if (($driverSettings['storage_driver'] ?? null) === 's3' && in_array(config('attachment.disk_explicit'), [null, ''], true)) {
|
||||
// storage_driver 가 `.env` 로 잠긴 설치에서는 저장값이 제거되어 있으므로 유효값으로
|
||||
// 판정한다. 보정하지 않으면 `FILESYSTEM_DISK=s3` 를 잠근 운영자의 첨부 업로드만
|
||||
// 로컬 디스크로 되돌아간다 (기존 행은 행 disk 로 서빙되므로 오류 없이 갈라진다).
|
||||
$storageDriver = EnvPriority::isLocked('drivers.storage_driver')
|
||||
? config('filesystems.default')
|
||||
: ($driverSettings['storage_driver'] ?? null);
|
||||
|
||||
if ($storageDriver === 's3' && in_array(config('attachment.disk_explicit'), [null, ''], true)) {
|
||||
Config::set('attachment.disk', 's3');
|
||||
}
|
||||
|
||||
@@ -372,7 +430,8 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
*/
|
||||
private function applyPublicAssetDiskConfig(JsonConfigRepository $configRepository): void
|
||||
{
|
||||
if (env('APP_ENV') === 'testing') {
|
||||
// env() 직접 호출은 config:cache 환경에서 null 로 고정되어 가드가 무력해진다.
|
||||
if (app()->environment('testing')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -389,6 +448,10 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
*/
|
||||
private function injectPublicAssetDiskConfig(JsonConfigRepository $configRepository): void
|
||||
{
|
||||
// audit:allow env-priority-filter-wiring 이 지점이 drivers 에서 읽는 키는
|
||||
// public_asset_disk 하나뿐이고 그 키는 EnvPriority::EXEMPT 다 (env 대응 없음).
|
||||
// 여기서 다른 drivers 키를 추가로 읽게 되면 그 키는 잠금을 우회하므로,
|
||||
// 그때는 이 면제를 걷어내고 filterLocked('drivers', …) 를 배선해야 한다.
|
||||
$driverSettings = $configRepository->getCategory('drivers');
|
||||
|
||||
$disk = (string) ($driverSettings['public_asset_disk'] ?? '');
|
||||
@@ -535,6 +598,14 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
*/
|
||||
private function applyWebsocketConfig(array $driverSettings): void
|
||||
{
|
||||
// 마스터 토글이 `.env` 로 잠긴 설치에서는 관리자 저장값이 이 배열에서 제거되어
|
||||
// 있으므로 `empty()` 가 참이 된다 — 그대로 두면 OFF 강제 3종이 오발동해
|
||||
// `.env`(BROADCAST_CONNECTION=reverb)로 웹소켓을 켠 운영자에게 강제 OFF 가 걸린다.
|
||||
// 잠긴 경우에는 OFF 강제도 ON 주입도 하지 않고 config 기본값(= `.env` 유래)을 서빙한다.
|
||||
if (EnvPriority::isLocked('drivers.websocket_enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($driverSettings['websocket_enabled'])) {
|
||||
Config::set('broadcasting.default', 'null');
|
||||
// 프론트(admin/app.blade.php)가 @if(broadcasting.connections.reverb.key)로 연결을
|
||||
@@ -562,13 +633,16 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
$serverHost = $driverSettings['websocket_server_host'] ?? '';
|
||||
$serverPort = (int) ($driverSettings['websocket_server_port'] ?? 0);
|
||||
$serverScheme = $driverSettings['websocket_server_scheme'] ?? '';
|
||||
if (empty($serverHost)) {
|
||||
// 잠긴 server 키에는 폴백을 태우지 않는다 — 클라이언트 endpoint 는 env 대응이 없는
|
||||
// 관리자 소유 값이라, 폴백을 그대로 두면 그 값이 `.env` 가 소유한 REVERB_HOST/PORT/SCHEME
|
||||
// 자리에 덮여 잠금이 무력해진다. 잠긴 키는 빈 값으로 남아 아래 주입에서 건너뛰어진다.
|
||||
if (empty($serverHost) && ! EnvPriority::isLocked('drivers.websocket_server_host')) {
|
||||
$serverHost = $clientHost;
|
||||
}
|
||||
if ($serverPort <= 0) {
|
||||
if ($serverPort <= 0 && ! EnvPriority::isLocked('drivers.websocket_server_port')) {
|
||||
$serverPort = $clientPort;
|
||||
}
|
||||
if (empty($serverScheme)) {
|
||||
if (empty($serverScheme) && ! EnvPriority::isLocked('drivers.websocket_server_scheme')) {
|
||||
$serverScheme = $clientScheme;
|
||||
}
|
||||
$serverUseTLS = $serverScheme === 'https';
|
||||
@@ -632,6 +706,9 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
return;
|
||||
}
|
||||
|
||||
// .env 우선 모드: `.env` 가 소유한 키 제거 (스위치 OFF 면 무동작).
|
||||
$settings = EnvPriority::filterLocked('core_update', $settings);
|
||||
|
||||
if (! empty($settings['github_url'])) {
|
||||
Config::set('app.update.github_url', $settings['github_url']);
|
||||
}
|
||||
@@ -658,6 +735,9 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
return;
|
||||
}
|
||||
|
||||
// .env 우선 모드: `.env` 가 소유한 키 제거 (스위치 OFF 면 무동작).
|
||||
$settings = EnvPriority::filterLocked('geoip', $settings);
|
||||
|
||||
if (isset($settings['feature_enabled'])) {
|
||||
Config::set('geoip.enabled', (bool) $settings['feature_enabled']);
|
||||
}
|
||||
@@ -691,6 +771,9 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
*/
|
||||
private function applyIdentityConfig(JsonConfigRepository $configRepository): void
|
||||
{
|
||||
// audit:allow env-priority-filter-wiring identity 카테고리는 전 키가 EnvPriority::EXEMPT 다
|
||||
// (env 대응이 없어 잠글 대상이 하나도 없다) — filterLocked 를 걸어도 no-op 이다.
|
||||
// 이 카테고리에 env 대응 키가 생기면 EnvPriorityContractTest 의 맵 패리티가 먼저 red 가 된다.
|
||||
$settings = $configRepository->getCategory('identity');
|
||||
|
||||
if (empty($settings)) {
|
||||
@@ -711,6 +794,9 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
return;
|
||||
}
|
||||
|
||||
// .env 우선 모드: `.env` 가 소유한 키 제거 (스위치 OFF 면 무동작).
|
||||
$uploadSettings = EnvPriority::filterLocked('upload', $uploadSettings);
|
||||
|
||||
// 관리자 설정은 MB, config/attachment.* 는 KB — 변환은 이 지점 단 한 곳에서만 수행한다.
|
||||
// (기존에는 존재하지 않는 키 `max_size` 를 읽어 설정이 어디에도 반영되지 않았다)
|
||||
if (! empty($uploadSettings['max_file_size'])) {
|
||||
|
||||
Reference in new issue
Block a user