request->getJSON(); $username = $json->username ?? ''; $password = $json->password ?? ''; if (empty($username) || empty($password)) { return $this->respondError('아이디와 비밀번호를 입력하세요.'); } // Find admin user (삭제된 계정 제외) $builder = $this->getDB()->table('admin_users'); $admin = $builder ->where('username', $username) ->where('deleted_YN', 'N') ->get()->getRow(); if (!$admin) { return $this->respondError('아이디 또는 비밀번호가 올바르지 않습니다.'); } // Check if account is locked (5 failed attempts) $loginAttempts = $admin->login_attempts ?? 0; if ($loginAttempts >= 5) { return $this->respondError('계정이 잠겼습니다. 슈퍼 관리자에게 문의하여주세요.', ResponseInterface::HTTP_FORBIDDEN); } // Check if admin is active if ($admin->status !== 'active') { return $this->respondError('비활성화된 계정입니다. 관리자에게 문의하세요.'); } // Verify password if (!password_verify($password, $admin->password)) { // Increment login attempts $newAttempts = $loginAttempts + 1; $updateBuilder = $this->getDB()->table('admin_users'); $updateBuilder->where('id', $admin->id)->update([ 'login_attempts' => $newAttempts, 'last_failed_login' => date('Y-m-d H:i:s') ]); // Check if account should be locked if ($newAttempts >= 5) { return $this->respondError('비밀번호를 5회 틀렸습니다. 계정이 잠겼습니다. 슈퍼 관리자에게 문의하여주세요.', ResponseInterface::HTTP_FORBIDDEN); } $remainingAttempts = 5 - $newAttempts; return $this->respondError("아이디 또는 비밀번호가 올바르지 않습니다. (남은 시도 횟수: {$remainingAttempts}회)"); } // Reset login attempts + last_login 업데이트 $resetBuilder = $this->getDB()->table('admin_users'); $resetBuilder->where('id', $admin->id)->update([ 'login_attempts' => 0, 'last_failed_login' => null, 'last_login' => date('Y-m-d H:i:s'), ]); // Generate token $token = bin2hex(random_bytes(32)); $expiresAt = date('Y-m-d H:i:s', strtotime('+24 hours')); // Save token $tokenBuilder = $this->getDB()->table('admin_tokens'); $tokenBuilder->insert([ 'admin_id' => $admin->id, 'token' => $token, 'expires_at' => $expiresAt, 'created_at' => date('Y-m-d H:i:s') ]); // Check if password change is needed (6 months) $passwordChangeNeeded = false; if (!empty($admin->password_changed_at)) { $sixMonthsAgo = strtotime('-6 months'); $passwordChangedTime = strtotime($admin->password_changed_at); $passwordChangeNeeded = $passwordChangedTime < $sixMonthsAgo; } else { // password_changed_at이 없으면 created_at 기준으로 체크 if (!empty($admin->created_at)) { $sixMonthsAgo = strtotime('-6 months'); $createdTime = strtotime($admin->created_at); $passwordChangeNeeded = $createdTime < $sixMonthsAgo; } } return $this->respondSuccess([ 'token' => $token, 'expires_at' => $expiresAt, 'password_change_needed' => $passwordChangeNeeded, 'admin' => [ 'id' => $admin->id, 'username' => $admin->username, 'name' => $admin->name, 'email' => $admin->email, 'role' => $admin->role, 'permissions' => $this->fetchPermissions((int) $admin->id, $admin->role), ] ], '로그인 성공'); } /** * 관리자 권한 배열 반환 — super_admin은 'all', admin은 메뉴 id 배열 */ private function fetchPermissions(int $adminId, string $role) { if ($role === 'super_admin') return 'all'; $rows = $this->getDB()->table('admin_permissions') ->select('permission') ->where('admin_id', $adminId) ->get()->getResult(); return array_map(fn($r) => $r->permission, $rows); } /** * Logout */ public function logout() { $tokenData = parent::validateToken(); if ($tokenData) { $builder = $this->getDB()->table('admin_tokens'); $builder->where('token', $tokenData->token)->delete(); } return $this->respondSuccess(null, '로그아웃 성공'); } /** * Check token (API endpoint) */ public function check() { $tokenData = $this->requireAuth(); if ($tokenData instanceof ResponseInterface) { return $tokenData; } // Get admin info $builder = $this->getDB()->table('admin_users'); $admin = $builder->where('id', $tokenData->admin_id)->get()->getRow(); return $this->respondSuccess([ 'admin' => [ 'id' => $admin->id, 'username' => $admin->username, 'name' => $admin->name, 'email' => $admin->email, 'role' => $admin->role, 'permissions' => $this->fetchPermissions((int) $admin->id, $admin->role), ] ]); } }