giftcon_dev/app/Http/Controllers/Admin/Auth/AdminAuthController.php
2026-02-04 16:55:00 +09:00

225 lines
8.3 KiB
PHP

<?php
namespace App\Http\Controllers\Admin\Auth;
use App\Http\Controllers\Controller;
use App\Rules\RecaptchaV3Rule;
use App\Services\Admin\AdminAuthService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
final class AdminAuthController extends Controller
{
public function __construct(
private readonly AdminAuthService $authService
) {}
public function showLogin()
{
return view('admin.auth.login');
}
public function storeLogin(Request $request)
{
$rules = [
'login_id' => ['required', 'string', 'max:190'], // admin_users.email(190)
'password' => ['required', 'string', 'max:255'],
'remember' => ['nullable'],
];
// 운영에서만 reCAPTCHA 필수
if (app()->environment('production')) {
$rules['g-recaptcha-response'] = ['required', new RecaptchaV3Rule('admin_login')];
}
$data = $request->validate($rules);
$email = strtolower(trim((string) $data['login_id']));
$remember = (bool) $request->boolean('remember');
$ip = (string) $request->ip();
$res = $this->authService->startLogin(
email: $email,
password: (string) $data['password'],
remember: $remember,
ip: $ip
);
if (($res['state'] ?? '') === 'invalid') {
return back()->withErrors(['login_id' => '이메일 또는 비밀번호를 확인하세요.'])->withInput();
}
if (($res['state'] ?? '') === 'sms_error') {
return back()->withErrors(['login_id' => '인증 sms 발송에 실패하였습니다.'])->withInput();
}
if (($res['state'] ?? '') === 'blocked') {
return back()->withErrors(['login_id' => '로그인 할 수 없는 계정입니다.'])->withInput();
}
if (($res['state'] ?? '') === 'must_reset') {
$request->session()->put('admin_pwreset', [
'admin_id' => (int) ($res['admin_id'] ?? 0),
'email' => $email,
'expires_at' => time() + 600, // 10분
'ip' => $ip,
]);
return redirect()->route('admin.password.reset.form')
->with('status', '비밀번호 초기화가 필요합니다. 새 비밀번호를 설정해 주세요.');
}
if (($res['state'] ?? '') !== 'otp_sent') {
// 방어: 예상치 못한 상태
return back()->withErrors(['login_id' => '로그인 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'])->withInput();
}
// ✅ OTP 챌린지 ID만 세션에 보관 (OTP 평문/해시 세션 저장 X)
$request->session()->put('admin_2fa', [
'challenge_id' => (string) ($res['challenge_id'] ?? ''),
'masked_phone' => (string) ($res['masked_phone'] ?? ''),
'expires_at' => time() + (int) config('admin.sms_ttl', 180),
]);
return redirect()->route('admin.otp.form')
->with('status', '인증번호를 문자로 발송했습니다.');
}
// 비밀번호 초기화 폼
public function showForceReset(Request $request)
{
$pending = (array) $request->session()->get('admin_pwreset', []);
if (empty($pending['admin_id'])) {
return redirect()->route('admin.login.form')
->withErrors(['login_id' => '비밀번호 초기화 세션이 없습니다. 다시 로그인해 주세요.']);
}
if ((int)($pending['expires_at'] ?? 0) < time()) {
$request->session()->forget('admin_pwreset');
return redirect()->route('admin.login.form')
->withErrors(['login_id' => '비밀번호 초기화가 만료되었습니다. 다시 로그인해 주세요.']);
}
return view('admin.auth.password_reset', [
'email' => (string)($pending['email'] ?? ''),
]);
}
// 비밀번호 초기화 저장
public function storeForceReset(Request $request)
{
$pending = (array) $request->session()->get('admin_pwreset', []);
if (empty($pending['admin_id'])) {
return redirect()->route('admin.login.form')
->withErrors(['login_id' => '비밀번호 초기화 세션이 없습니다. 다시 로그인해 주세요.']);
}
if ((int)($pending['expires_at'] ?? 0) < time()) {
$request->session()->forget('admin_pwreset');
return redirect()->route('admin.login.form')
->withErrors(['login_id' => '비밀번호 초기화가 만료되었습니다. 다시 로그인해 주세요.']);
}
$data = $request->validate([
'password' => ['required', 'string', 'min:10', 'max:255', 'confirmed'],
]);
$ip = (string) $request->ip();
$res = $this->authService->resetPassword(
adminId: (int) $pending['admin_id'],
newPassword: (string) $data['password'],
ip: $ip
);
if (!($res['ok'] ?? false)) {
$request->session()->forget('admin_pwreset');
return redirect()->route('admin.login.form')
->withErrors(['login_id' => '비밀번호 변경에 실패했습니다. 다시 로그인해 주세요.']);
}
$request->session()->forget('admin_pwreset');
return redirect()->route('admin.login.form')
->with('status', '비밀번호가 변경되었습니다. 다시 로그인해 주세요.');
}
public function showOtp(Request $request)
{
$pending = (array) $request->session()->get('admin_2fa', []);
if (empty($pending['challenge_id'])) {
return redirect()->route('admin.login.form')
->withErrors(['login_id' => '인증 정보가 없습니다. 다시 로그인해 주세요.']);
}
if ((int)($pending['expires_at'] ?? 0) < time()) {
$request->session()->forget('admin_2fa');
return redirect()->route('admin.login.form')
->withErrors(['login_id' => '인증이 만료되었습니다. 다시 로그인해 주세요.']);
}
return view('admin.auth.otp', [
'masked_phone' => (string)($pending['masked_phone'] ?? ''),
]);
}
public function verifyOtp(Request $request)
{
$data = $request->validate([
'otp' => ['required', 'digits:6'],
]);
$pending = (array) $request->session()->get('admin_2fa', []);
$challengeId = (string)($pending['challenge_id'] ?? '');
if ($challengeId === '') {
return redirect()->route('admin.login.form')
->withErrors(['login_id' => '인증 정보가 없습니다. 다시 로그인해 주세요.']);
}
$res = $this->authService->verifyOtp(
challengeId: $challengeId,
otp: (string) $data['otp'],
ip: (string) $request->ip()
);
if (!($res['ok'] ?? false)) {
$reason = (string)($res['reason'] ?? '');
$msg = match ($reason) {
'expired' => '인증이 만료되었습니다. 다시 로그인해 주세요.',
'attempts' => '시도 횟수를 초과했습니다. 다시 로그인해 주세요.',
'ip' => '접속 정보가 변경되었습니다. 다시 로그인해 주세요.',
'blocked' => '로그인 할 수 없는 계정입니다.',
default => '인증번호가 올바르지 않습니다.',
};
if ($reason !== 'invalid') {
$request->session()->forget('admin_2fa');
return redirect()->route('admin.login.form')->withErrors(['login_id' => $msg]);
}
return back()->withErrors(['otp' => $msg]);
}
/** @var \App\Models\AdminUser $admin */
$admin = $res['admin'];
Auth::guard('admin')->login($admin, (bool)($res['remember'] ?? false));
$request->session()->forget('admin_2fa');
return redirect()->route('admin.home');
}
public function logout(Request $request)
{
Auth::guard('admin')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('admin.login.form');
}
}