giftcon_dev/database/seeders/AdminRbacSeeder.php

299 lines
11 KiB
PHP

<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
final class AdminRbacSeeder extends Seeder
{
/** @var array<string, array<int, string>> */
private array $colsCache = [];
public function run(): void
{
// ===== 0) 테이블 존재 체크 =====
foreach (['admin_roles', 'admin_permissions', 'admin_permission_role', 'admin_users', 'admin_role_user'] as $t) {
if (!Schema::hasTable($t)) {
throw new \RuntimeException("Missing table: {$t}");
}
}
// ===== 1) 역할(roles) =====
// 현재 스키마가 name/label 기반(코드 없음)인 것에 맞춰 유지
$roles = [
['name' => 'super_admin', 'label' => '최고관리자'],
['name' => 'finance', 'label' => '정산관리'],
['name' => 'product', 'label' => '상품관리'],
['name' => 'support', 'label' => 'CS/상담'],
];
foreach ($roles as $r) {
$payload = ['label' => $r['label']];
$this->putTs($payload, 'admin_roles');
DB::table('admin_roles')->updateOrInsert(
['name' => $r['name']],
$payload
);
}
// ===== 2) 권한(permissions) =====
$perms = [
['name' => 'admin.access', 'label' => '관리자 접근'],
['name' => 'settlement.manage', 'label' => '정산 관리'],
['name' => 'product.manage', 'label' => '상품 관리'],
['name' => 'support.manage', 'label' => 'CS/상담 관리'],
['name' => 'member.manage', 'label' => '회원 관리'],
];
foreach ($perms as $p) {
$payload = ['label' => $p['label']];
$this->putTs($payload, 'admin_permissions');
DB::table('admin_permissions')->updateOrInsert(
['name' => $p['name']],
$payload
);
}
// ===== 3) 역할별 권한 매핑 =====
// super_admin = 전체 권한
// 일반 role = admin.access + 해당 도메인 권한
$roleId = fn(string $name): int => (int) DB::table('admin_roles')->where('name', $name)->value('id');
$permId = fn(string $name): int => (int) DB::table('admin_permissions')->where('name', $name)->value('id');
$superRoleId = $roleId('super_admin');
$financeRoleId = $roleId('finance');
$productRoleId = $roleId('product');
$supportRoleId = $roleId('support');
$allPermIds = DB::table('admin_permissions')->pluck('id')->map(fn($v) => (int)$v)->all();
foreach ($allPermIds as $pid) {
$this->upsertPivot('admin_permission_role', [
'admin_permission_id' => $pid,
'admin_role_id' => $superRoleId,
]);
}
// 일반 역할 권한
$this->upsertPivot('admin_permission_role', [
'admin_permission_id' => $permId('admin.access'),
'admin_role_id' => $financeRoleId,
]);
$this->upsertPivot('admin_permission_role', [
'admin_permission_id' => $permId('settlement.manage'),
'admin_role_id' => $financeRoleId,
]);
$this->upsertPivot('admin_permission_role', [
'admin_permission_id' => $permId('admin.access'),
'admin_role_id' => $productRoleId,
]);
$this->upsertPivot('admin_permission_role', [
'admin_permission_id' => $permId('product.manage'),
'admin_role_id' => $productRoleId,
]);
$this->upsertPivot('admin_permission_role', [
'admin_permission_id' => $permId('admin.access'),
'admin_role_id' => $supportRoleId,
]);
$this->upsertPivot('admin_permission_role', [
'admin_permission_id' => $permId('support.manage'),
'admin_role_id' => $supportRoleId,
]);
// ===== 4) super_admin 유저 1명 보장(기존 로직 유지) =====
$seedEmail = (string) env('ADMIN_SEED_EMAIL', 'sungro815@syye.net');
$seedPw = (string) env('ADMIN_SEED_PASSWORD', 'tjekdfl1324%^');
$seedName = (string) env('ADMIN_SEED_NAME', 'Super Admin');
$seedPhone = (string) env('ADMIN_SEED_PHONE', '01036828958');
$this->ensureUserWithRole(
email: $seedEmail,
passwordPlain: $seedPw,
name: $seedName,
nickname: '최고관리자',
phoneRaw: $seedPhone,
roleId: $superRoleId,
// super_admin은 phone_hash를 써도 되지만, 스키마/정책 따라 충돌 피하려면 null도 OK
usePhoneHash: true
);
// ===== 5) 일반 관리자 10명 생성 =====
$fixedPw = 'tjekdfl1324%^';
$fixedPhone = '01036828958';
$koreanNames = [
'김민준', '이서연', '박지훈', '최유진', '정현우',
'한지민', '강다은', '조성훈', '윤하늘', '오지후',
];
// 역할 배치: 1~3 finance / 4~6 product / 7~10 support
for ($i = 1; $i <= 10; $i++) {
$email = "admin{$i}@mail.com";
$name = $koreanNames[$i - 1];
if ($i <= 3) {
$rid = $financeRoleId;
$nick = "정산담당{$i}";
} elseif ($i <= 6) {
$rid = $productRoleId;
$nick = "상품담당".($i - 3);
} else {
$rid = $supportRoleId;
$nick = "CS담당".($i - 6);
}
// 일반 관리자들은 동일 전화번호 사용 → phone_hash 충돌 방지 위해 usePhoneHash=false(=NULL 저장)
$this->ensureUserWithRole(
email: $email,
passwordPlain: $fixedPw,
name: $name,
nickname: $nick,
phoneRaw: $fixedPhone,
roleId: $rid,
usePhoneHash: false
);
}
}
/**
* 유저 생성/갱신 + 역할 부여(스키마 컬럼/피벗 컬럼이 달라도 안전하게)
*/
private function ensureUserWithRole(
string $email,
string $passwordPlain,
string $name,
string $nickname,
string $phoneRaw,
int $roleId,
bool $usePhoneHash
): void {
$hashKey = (string) config('admin.phone_hash_key', env('ADMIN_PHONE_HASH_KEY', ''));
$phoneDigits = preg_replace('/\D+/', '', $phoneRaw) ?? '';
$phoneEnc = $phoneDigits !== '' ? Crypt::encryptString($phoneDigits) : null;
$last4 = $phoneDigits !== '' ? substr($phoneDigits, -4) : null;
$hashKey = (string) config('admin.phone_hash_key', env('ADMIN_PHONE_HASH_KEY', ''));
if ($hashKey === '') {
throw new \RuntimeException('ADMIN_PHONE_HASH_KEY (admin.phone_hash_key) is empty. Set it in .env');
}
$phoneDigits = preg_replace('/\D+/', '', $phoneRaw) ?? '';
if ($phoneDigits === '') {
throw new \RuntimeException('Seed phone is empty');
}
/**
* ✅ phone_hash는 NOT NULL이므로 항상 채운다.
* - 운영 정책용(=진짜 조회키)일 때: phoneDigits만
* - seed에서 같은 번호 10명 만들 때: phoneDigits + email로 유니크하게
*/
$hashInput = $usePhoneHash
? $phoneDigits
: ($phoneDigits . '|seed|' . $email);
$phoneHash = hash_hmac('sha256', $hashInput, $hashKey);
$exists = DB::table('admin_users')->where('email', $email)->first();
if (!$exists) {
$insert = [];
$this->putIfCol($insert, 'admin_users', 'email', $email);
$this->putIfCol($insert, 'admin_users', 'password', Hash::make($passwordPlain));
$this->putIfCol($insert, 'admin_users', 'name', $name);
$this->putIfCol($insert, 'admin_users', 'nickname', $nickname);
$this->putIfCol($insert, 'admin_users', 'phone_enc', $phoneEnc);
$this->putIfCol($insert, 'admin_users', 'phone_hash', $phoneHash); // 일반 관리자는 NULL
$this->putIfCol($insert, 'admin_users', 'phone_last4', $last4);
$this->putIfCol($insert, 'admin_users', 'status', 'active');
$this->putIfCol($insert, 'admin_users', 'must_reset_password', 0);
$this->putIfCol($insert, 'admin_users', 'two_factor_mode', 'sms');
$this->putIfCol($insert, 'admin_users', 'totp_enabled', 0);
$this->putIfCol($insert, 'admin_users', 'failed_login_count', 0);
$this->putIfCol($insert, 'admin_users', 'locked_until', null);
$this->putIfCol($insert, 'admin_users', 'remember_token', null);
$this->putIfCol($insert, 'admin_users', 'deleted_at', null);
$this->putTs($insert, 'admin_users');
$adminUserId = DB::table('admin_users')->insertGetId($insert);
} else {
$adminUserId = (int) $exists->id;
// 이미 있으면 갱신(비번/이름/닉/폰)
$update = [];
$this->putIfCol($update, 'admin_users', 'password', Hash::make($passwordPlain));
$this->putIfCol($update, 'admin_users', 'name', $name);
$this->putIfCol($update, 'admin_users', 'nickname', $nickname);
$this->putIfCol($update, 'admin_users', 'phone_enc', $phoneEnc);
$this->putIfCol($update, 'admin_users', 'phone_hash', $phoneHash);
$this->putIfCol($update, 'admin_users', 'phone_last4', $last4);
$this->putIfCol($update, 'admin_users', 'status', 'active');
$this->putTs($update, 'admin_users', onlyUpdated: true);
if (!empty($update)) {
DB::table('admin_users')->where('id', $adminUserId)->update($update);
}
}
// 역할 부여
$this->upsertPivot('admin_role_user', [
'admin_user_id' => $adminUserId,
'admin_role_id' => $roleId,
]);
}
/**
* Pivot upsert (assigned_at/created_at/updated_at 등 존재하면 자동 채움)
*/
private function upsertPivot(string $table, array $keys): void
{
$values = [];
// pivot에 assigned_at 같은 NOT NULL이 있을 수 있어 자동 세팅
if ($this->hasCol($table, 'assigned_at')) $values['assigned_at'] = now();
if ($this->hasCol($table, 'assigned_by')) $values['assigned_by'] = null;
if ($this->hasCol($table, 'created_at')) $values['created_at'] = now();
if ($this->hasCol($table, 'updated_at')) $values['updated_at'] = now();
DB::table($table)->updateOrInsert($keys, $values);
}
private function putTs(array &$payload, string $table, bool $onlyUpdated = false): void
{
if ($this->hasCol($table, 'updated_at')) $payload['updated_at'] = now();
if (!$onlyUpdated && $this->hasCol($table, 'created_at')) $payload['created_at'] = now();
}
private function putIfCol(array &$payload, string $table, string $col, mixed $value): void
{
if ($this->hasCol($table, $col)) {
$payload[$col] = $value;
}
}
private function hasCol(string $table, string $col): bool
{
if (!isset($this->colsCache[$table])) {
$this->colsCache[$table] = Schema::getColumnListing($table);
}
return in_array($col, $this->colsCache[$table], true);
}
}