giftcon_dev/app/Repositories/Admin/AdminUserRepository.php
2026-02-11 16:17:15 +09:00

563 lines
19 KiB
PHP

<?php
namespace App\Repositories\Admin;
use App\Models\AdminUser;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
final class AdminUserRepository
{
/** @var array<string, array<int, string>> */
private array $colsCache = [];
/** @var array<string, string|null> */
private array $colTypeCache = [];
// =========================
// Basic Find
// =========================
public function findByEmail(string $email): ?AdminUser
{
return AdminUser::query()
->where('email', $email)
->first();
}
public function findActiveById(int $id): ?AdminUser
{
return AdminUser::query()
->whereKey($id)
->where('status', 'active')
->first();
}
public function find(int $id): ?object
{
return DB::table('admin_users')->where('id', $id)->first();
}
// =========================
// Login Touch
// =========================
public function touchLogin(AdminUser $admin, string $ip, ?string $ua = null): void
{
$data = [
'last_login_at' => now(),
];
// last_login_ip 타입이 BINARY일 수도, VARCHAR일 수도 있어서 타입별 처리
if ($this->hasCol('admin_users', 'last_login_ip')) {
$type = $this->getColumnType('admin_users', 'last_login_ip'); // e.g. binary, varbinary, varchar...
if ($type && (str_contains($type, 'binary') || str_contains($type, 'blob'))) {
$data['last_login_ip'] = inet_pton($ip) ?: null;
} else {
$data['last_login_ip'] = $ip ?: null;
}
}
if ($ua !== null && $this->hasCol('admin_users', 'last_login_ua')) {
$data['last_login_ua'] = $ua;
}
$this->save($admin, $data);
}
// =========================
// Password
// =========================
public function setPassword(AdminUser $admin, string $plainPassword): void
{
// ✅ AdminUser 모델에 password cast(hashed)가 있으면 plain을 넣어도 해싱됨
$data = [
'password' => $plainPassword,
'must_reset_password' => 0,
'password_changed_at' => now(),
];
$this->save($admin, $data);
}
public function setTemporaryPassword(AdminUser $admin, string $plainPassword, int $actorId = 0): void
{
$data = [
'password' => $plainPassword, // 모델 cast가 해싱
'must_reset_password' => 1,
'password_changed_at' => now(),
'updated_by' => $actorId ?: null,
];
$this->save($admin, $data);
}
// =========================
// Update (DB safe)
// =========================
public function updateById(int $id, array $data): bool
{
$data['updated_at'] = $data['updated_at'] ?? now();
$data = $this->filterToExistingColumns('admin_users', $data);
return DB::table('admin_users')
->where('id', $id)
->update($data) > 0;
}
/** 기존 코드 호환용 */
public function update(int $id, array $data): bool
{
return $this->updateById($id, $data);
}
public function save(AdminUser $admin, array $data): void
{
$data = $this->filterToExistingColumns('admin_users', $data);
foreach ($data as $k => $v) {
$admin->{$k} = $v;
}
$admin->save();
}
public function existsPhoneHash(string $hash, int $ignoreId): bool
{
return DB::table('admin_users')
->whereNull('deleted_at')
->where('phone_hash', $hash)
->where('id', '!=', $ignoreId)
->exists();
}
// =========================
// List / Search
// =========================
public function paginateUsers(array $filters, int $perPage = 20): LengthAwarePaginator
{
$q = AdminUser::query();
$keyword = trim((string)($filters['q'] ?? ''));
if ($keyword !== '') {
$q->where(function ($w) use ($keyword) {
if ($this->hasCol('admin_users', 'email')) $w->orWhere('email', 'like', "%{$keyword}%");
if ($this->hasCol('admin_users', 'name')) $w->orWhere('name', 'like', "%{$keyword}%");
if ($this->hasCol('admin_users', 'nickname')) $w->orWhere('nickname', 'like', "%{$keyword}%");
});
}
$status = (string)($filters['status'] ?? '');
if ($status !== '' && $this->hasCol('admin_users', 'status')) {
$q->where('status', $status);
}
$role = (string)($filters['role'] ?? '');
if ($role !== '' && Schema::hasTable('admin_role_user') && Schema::hasTable('admin_roles')) {
$roleNameCol = $this->firstExistingColumn('admin_roles', ['name','code','slug']);
if ($roleNameCol) {
$q->whereIn('id', function ($sub) use ($role, $roleNameCol) {
$sub->select('aru.admin_user_id')
->from('admin_role_user as aru')
->join('admin_roles as r', 'r.id', '=', 'aru.admin_role_id')
->where("r.{$roleNameCol}", $role);
});
}
}
if ($this->hasCol('admin_users', 'last_login_at')) {
$q->orderByDesc('last_login_at');
} else {
$q->orderByDesc('id');
}
return $q->paginate($perPage)->withQueryString();
}
// =========================
// Roles / Permissions
// (현재 DB: roles.name/label, perms.name/label 기반도 지원)
// =========================
public function getAllRoles(): array
{
if (!Schema::hasTable('admin_roles')) return [];
$codeCol = $this->firstExistingColumn('admin_roles', ['code','name','slug']);
$nameCol = $this->firstExistingColumn('admin_roles', ['label','title','name']);
return DB::table('admin_roles')
->orderBy('id')
->get([
'id',
DB::raw(($codeCol ? "`{$codeCol}`" : "CAST(id AS CHAR)") . " as code"),
DB::raw(($nameCol ? "`{$nameCol}`" : "CAST(id AS CHAR)") . " as name"),
])
->map(fn($r)=>(array)$r)
->all();
}
public function findRoleIdByCode(string $code): ?int
{
if (!Schema::hasTable('admin_roles')) return null;
$codeCol = $this->firstExistingColumn('admin_roles', ['code','name','slug']);
if (!$codeCol) return null;
$id = DB::table('admin_roles')->where($codeCol, $code)->value('id');
return $id ? (int)$id : null;
}
public function getRoleIdsForUser(int $adminUserId): array
{
if (!Schema::hasTable('admin_role_user')) return [];
return DB::table('admin_role_user')
->where('admin_user_id', $adminUserId)
->pluck('admin_role_id')
->map(fn($v)=>(int)$v)
->all();
}
/** ✅ (중복 선언 금지) 상세/내정보/관리페이지 공용 */
public function getRolesForUser(int $adminUserId): array
{
if (!Schema::hasTable('admin_roles') || !Schema::hasTable('admin_role_user')) return [];
$codeCol = $this->firstExistingColumn('admin_roles', ['code','name','slug']);
$nameCol = $this->firstExistingColumn('admin_roles', ['label','title','name']);
return DB::table('admin_role_user as aru')
->join('admin_roles as r', 'r.id', '=', 'aru.admin_role_id')
->where('aru.admin_user_id', $adminUserId)
->orderBy('r.id')
->get([
'r.id',
DB::raw(($codeCol ? "r.`{$codeCol}`" : "CAST(r.id AS CHAR)") . " as code"),
DB::raw(($nameCol ? "r.`{$nameCol}`" : "CAST(r.id AS CHAR)") . " as name"),
])
->map(fn($row)=>(array)$row)
->all();
}
public function getRoleMapForUsers(array $userIds): array
{
if (empty($userIds)) return [];
if (!Schema::hasTable('admin_role_user') || !Schema::hasTable('admin_roles')) return [];
$codeCol = $this->firstExistingColumn('admin_roles', ['code','name','slug']);
$nameCol = $this->firstExistingColumn('admin_roles', ['label','title','name']);
$rows = DB::table('admin_role_user as aru')
->join('admin_roles as r', 'r.id', '=', 'aru.admin_role_id')
->whereIn('aru.admin_user_id', $userIds)
->get([
'aru.admin_user_id as uid',
'r.id as rid',
DB::raw(($codeCol ? "r.`{$codeCol}`" : "CAST(r.id AS CHAR)") . " as code"),
DB::raw(($nameCol ? "r.`{$nameCol}`" : "CAST(r.id AS CHAR)") . " as name"),
]);
$map = [];
foreach ($rows as $row) {
$uid = (int)$row->uid;
$map[$uid] ??= [];
$map[$uid][] = ['id'=>(int)$row->rid,'code'=>(string)$row->code,'name'=>(string)$row->name];
}
return $map;
}
public function getPermissionsForUser(int $adminUserId): array
{
if (!Schema::hasTable('admin_role_user') || !Schema::hasTable('admin_permission_role') || !Schema::hasTable('admin_permissions')) {
return [];
}
$codeCol = $this->firstExistingColumn('admin_permissions', ['code','name','slug']);
$nameCol = $this->firstExistingColumn('admin_permissions', ['label','title','name']);
return DB::table('admin_role_user as aru')
->join('admin_permission_role as apr', 'apr.admin_role_id', '=', 'aru.admin_role_id')
->join('admin_permissions as p', 'p.id', '=', 'apr.admin_permission_id')
->where('aru.admin_user_id', $adminUserId)
->distinct()
->orderBy('p.id')
->get([
'p.id',
DB::raw(($codeCol ? "p.`{$codeCol}`" : "CAST(p.id AS CHAR)") . " as code"),
DB::raw(($nameCol ? "p.`{$nameCol}`" : "CAST(p.id AS CHAR)") . " as name"),
])
->map(fn($row)=>(array)$row)
->all();
}
public function getPermissionCountMapForUsers(array $userIds): array
{
if (empty($userIds)) return [];
if (!Schema::hasTable('admin_role_user') || !Schema::hasTable('admin_permission_role')) return [];
$rows = DB::table('admin_role_user as aru')
->join('admin_permission_role as apr', 'apr.admin_role_id', '=', 'aru.admin_role_id')
->whereIn('aru.admin_user_id', $userIds)
->select('aru.admin_user_id as uid', DB::raw('COUNT(DISTINCT apr.admin_permission_id) as cnt'))
->groupBy('aru.admin_user_id')
->get();
$map = [];
foreach ($rows as $r) $map[(int)$r->uid] = (int)$r->cnt;
return $map;
}
public function syncRoles(int $adminUserId, array $roleIds, int $actorId): void
{
if (!Schema::hasTable('admin_role_user')) return;
$roleIds = array_values(array_unique(array_map('intval', $roleIds)));
DB::table('admin_role_user')
->where('admin_user_id', $adminUserId)
->whereNotIn('admin_role_id', $roleIds)
->delete();
$existing = DB::table('admin_role_user')
->where('admin_user_id', $adminUserId)
->pluck('admin_role_id')
->map(fn($v)=>(int)$v)->all();
$need = array_values(array_diff($roleIds, $existing));
foreach ($need as $rid) {
$row = [
'admin_user_id' => $adminUserId,
'admin_role_id' => (int)$rid,
];
if ($this->hasCol('admin_role_user', 'assigned_by')) $row['assigned_by'] = $actorId;
if ($this->hasCol('admin_role_user', 'assigned_at')) $row['assigned_at'] = now();
DB::table('admin_role_user')->insert($row);
}
}
// =========================
// Helpers
// =========================
private function filterToExistingColumns(string $table, array $data): array
{
$cols = $this->cols($table);
$out = [];
foreach ($data as $k => $v) {
if (in_array($k, $cols, true)) {
// null 허용 컬럼만 null 넣고 싶으면 여기서 정책 추가 가능
$out[$k] = $v;
}
}
return $out;
}
private function cols(string $table): array
{
if (!isset($this->colsCache[$table])) {
$this->colsCache[$table] = Schema::hasTable($table) ? Schema::getColumnListing($table) : [];
}
return $this->colsCache[$table];
}
private function hasCol(string $table, string $col): bool
{
return in_array($col, $this->cols($table), true);
}
private function firstExistingColumn(string $table, array $candidates): ?string
{
$cols = $this->cols($table);
foreach ($candidates as $c) {
if (in_array($c, $cols, true)) return $c;
}
return null;
}
private function getColumnType(string $table, string $col): ?string
{
$key = "{$table}.{$col}";
if (array_key_exists($key, $this->colTypeCache)) {
return $this->colTypeCache[$key];
}
try {
$row = DB::selectOne("SHOW COLUMNS FROM `{$table}` LIKE ?", [$col]);
$type = $row?->Type ?? null; // e.g. varbinary(16), varchar(45)
$this->colTypeCache[$key] = $type ? strtolower((string)$type) : null;
return $this->colTypeCache[$key];
} catch (\Throwable $e) {
$this->colTypeCache[$key] = null;
return null;
}
}
public function bumpLoginFail(int $id, int $limit = 3): array
{
return DB::transaction(function () use ($id, $limit) {
$row = DB::table('admin_users')
->select('failed_login_count', 'locked_until')
->where('id', $id)
->lockForUpdate()
->first();
$cur = (int)($row->failed_login_count ?? 0);
$next = $cur + 1;
$locked = ($row->locked_until ?? null) !== null;
$update = [
'failed_login_count' => $next,
'updated_at' => now(),
];
// ✅ 3회 이상이면 "영구잠금"
if (!$locked && $next >= $limit) {
$update['locked_until'] = now();
$locked = true;
}
DB::table('admin_users')->where('id', $id)->update($update);
return ['count' => $next, 'locked' => $locked];
});
}
/**
* 로그인 성공 시 실패 카운터/잠금 초기화
*/
public function clearLoginFailAndUnlock(int $id): void
{
DB::table('admin_users')
->where('id', $id)
->update([
'failed_login_count' => 0,
'locked_until' => null,
'updated_at' => now(),
]);
}
public function existsByEmail(string $email): bool
{
return DB::table('admin_users')->where('email', $email)->exists();
}
public function existsByPhoneHash(string $phoneHash): bool
{
return DB::table('admin_users')->where('phone_hash', $phoneHash)->exists();
}
public function insertAdminUser(array $data): int
{
// admin_users 컬럼 그대로 사용 (DB 구조 변경 X)
$id = DB::table('admin_users')->insertGetId($data);
return (int)$id;
}
public function findRoleByCode(string $code): ?array
{
$row = DB::table('admin_roles')->where('name', $code)->first();
if (!$row) return null;
return (array)$row;
}
public function deleteRoleMappingsExcept(int $adminId, int $roleId): void
{
DB::table('admin_role_user')
->where('admin_user_id', $adminId)
->where('admin_role_id', '!=', $roleId)
->delete();
}
public function upsertRoleMapping(int $adminId, int $roleId): void
{
$table = 'admin_role_user';
$vals = [];
$now = now();
// 컬럼 있으면 채움
if (Schema::hasColumn($table, 'assigned_at')) $vals['assigned_at'] = $now;
if (Schema::hasColumn($table, 'created_at')) $vals['created_at'] = $now;
if (Schema::hasColumn($table, 'updated_at')) $vals['updated_at'] = $now;
DB::table($table)->updateOrInsert(
['admin_user_id' => $adminId, 'admin_role_id' => $roleId],
$vals
);
}
public function updateTotpStart(int $id, string $secretEnc): bool
{
return DB::table('admin_users')->where('id', $id)->update([
'totp_secret_enc' => $secretEnc,
'totp_verified_at' => null,
'totp_enabled' => 0, // 등록 완료 전에는 SMS로 유지
'updated_at' => now(),
]) > 0;
}
public function confirmTotp(int $id): bool
{
return DB::table('admin_users')->where('id', $id)->update([
'totp_verified_at' => now(),
'totp_enabled' => 1, // 등록 완료되면 OTP 사용으로 전환(원하면 0 유지로 바꿔도 됨)
'updated_at' => now(),
]) > 0;
}
public function disableTotp(int $id): bool
{
return DB::table('admin_users')->where('id', $id)->update([
'totp_secret_enc' => null,
'totp_verified_at' => null,
'totp_enabled' => 0,
'updated_at' => now(),
]) > 0;
}
public function updateTotpMode(int $id, int $enabled): bool
{
return DB::table('admin_users')->where('id', $id)->update([
'totp_enabled' => $enabled ? 1 : 0,
'updated_at' => now(),
]) > 0;
}
public function getEmailMapByIds(array $ids): array
{
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v)=>$v>0)));
if (empty($ids)) return [];
// id => email
return DB::table('admin_users')
->whereIn('id', $ids)
->pluck('email', 'id')
->map(fn($v)=>(string)$v)
->all();
}
public function getMetaMapByIds(array $ids): array
{
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v)=>$v>0)));
if (empty($ids)) return [];
$rows = DB::table('admin_users')
->whereIn('id', $ids)
->get(['id','email','name','nickname']);
$map = [];
foreach ($rows as $r) {
$id = (int)$r->id;
$map[$id] = [
'id' => (int)$r->id,
'email' => (string)($r->email ?? ''),
'name' => (string)($r->name ?? ''),
'nick' => (string)($r->nickname ?? ''),
];
}
return $map;
}
}