299 lines
11 KiB
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);
|
|
}
|
|
}
|