giftcon_dev/app/Http/Controllers/Web/Auth/FindPasswordController.php
2026-01-15 11:15:26 +09:00

262 lines
9.9 KiB
PHP

<?php
namespace App\Http\Controllers\Web\Auth;
namespace App\Http\Controllers\Web\Auth;
use App\Services\MailService;
use App\Http\Controllers\Controller;
use App\Models\MemInfo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
class FindPasswordController extends Controller
{
public function show(Request $request)
{
$sess = $request->session()->get('find_pw', []);
$step = 1;
if (!empty($sess['verified'])) $step = 3;
else if (!empty($sess['sent'])) $step = 2;
return view('web.auth.find_password', [
'initialStep' => $step,
'email' => $sess['email'] ?? null,
]);
}
public function sendCode(Request $request)
{
$v = Validator::make($request->all(), [
'email' => ['required', 'email', 'max:120'],
], [
'email.required' => '이메일을 입력해 주세요.',
'email.email' => '이메일 형식이 올바르지 않습니다.',
]);
if ($v->fails()) {
return response()->json(['ok' => false, 'message' => $v->errors()->first()], 422);
}
$email = mb_strtolower(trim((string) $request->input('email')));
// 0) 가입된 이메일인지 확인 (MemInfo 기준)
$exists = MemInfo::query()
->whereNotNull('email')
->where('email', '<>', '')
->whereRaw('LOWER(email) = ?', [$email])
->exists();
if (!$exists) {
return response()->json([
'ok' => false,
'code' => 'EMAIL_NOT_FOUND',
'message' => '해당 이메일로 가입된 계정을 찾을 수 없습니다. 이메일을 다시 확인해 주세요.',
'step' => 1,
], 404);
}
try {
app(MailService::class)->sendTemplate(
$email,
'[PIN FOR YOU] 비밀번호 재설정 인증번호',
'mail.legacy.noti_email_auth_1', // CI 템플릿명에 맞춰 선택
[
'code' => $code,
'expires_min' => 3,
'email' => $email,
],
queue: true
);
} catch (\Throwable $e) {
$request->session()->forget('find_pw');
Log::error('FindPassword sendCode failed', ['email' => $email, 'error' => $e->getMessage()]);
return response()->json(['ok'=>false,'message'=>'인증번호 발송 중 오류가 발생했습니다.'], 500);
}
// 1) 레이트리밋(이메일 기준)
$key = 'findpw:send:' . $email;
if (RateLimiter::tooManyAttempts($key, 5)) { // 10분 5회 예시
$sec = RateLimiter::availableIn($key);
return response()->json(['ok' => false, 'message' => "요청이 너무 많습니다. {$sec}초 후 다시 시도해 주세요."], 429);
}
RateLimiter::hit($key, 600);
// 2) OTP 생성
$code = (string) random_int(100000, 999999);
// 3) 세션 저장 (코드는 해시로)
$request->session()->put('find_pw', [
'sent' => true,
'email' => $email,
'code' => password_hash($code, PASSWORD_DEFAULT),
'code_expires_at' => now()->addMinutes(3)->timestamp,
'verified' => false,
'verified_at' => null,
]);
// 4) 실제 발송 연결 (메일/SMS 등)
try {
// TODO: 프로젝트에 맞게 구현
// 예: Mail::to($email)->send(new PasswordOtpMail($code));
} catch (\Throwable $e) {
$request->session()->forget('find_pw');
Log::error('FindPassword sendCode failed', [
'email' => $email,
'error' => $e->getMessage(),
]);
return response()->json([
'ok' => false,
'message' => '인증번호 발송 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
], 500);
}
$isLocal = app()->environment(['local', 'development', 'testing']);
return response()->json([
'ok' => true,
'message' => '인증번호를 발송했습니다.',
'expires_in' => 180,
'step' => 2,
'dev_code' => $isLocal ? $code : null,
]);
}
public function verify(Request $request)
{
$v = Validator::make($request->all(), [
'code' => ['required', 'digits:6'],
], [
'code.required' => '인증번호를 입력해 주세요.',
'code.digits' => '인증번호 6자리를 입력해 주세요.',
]);
if ($v->fails()) {
return response()->json(['ok' => false, 'message' => $v->errors()->first()], 422);
}
$sess = $request->session()->get('find_pw');
if (!$sess || empty($sess['sent']) || empty($sess['email'])) {
return response()->json(['ok' => false, 'message' => '먼저 인증번호를 요청해 주세요.'], 400);
}
if (empty($sess['code_expires_at']) || now()->timestamp > (int)$sess['code_expires_at']) {
return response()->json(['ok' => false, 'message' => '인증번호가 만료되었습니다. 다시 요청해 주세요.'], 400);
}
// 검증 시도 레이트리밋
$key = 'findpw:verify:' . $sess['email'];
if (RateLimiter::tooManyAttempts($key, 10)) { // 10분 10회 예시
$sec = RateLimiter::availableIn($key);
return response()->json(['ok' => false, 'message' => "시도 횟수가 많습니다. {$sec}초 후 다시 시도해 주세요."], 429);
}
RateLimiter::hit($key, 600);
$code = (string) $request->input('code');
$ok = password_verify($code, $sess['code'] ?? '');
if (!$ok) {
return response()->json(['ok' => false, 'message' => '인증번호가 일치하지 않습니다.'], 422);
}
// 인증 성공 → step3 허용
$sess['verified'] = true;
$sess['verified_at'] = now()->timestamp;
$request->session()->put('find_pw', $sess);
return response()->json([
'ok' => true,
'message' => '인증이 완료되었습니다. 새 비밀번호를 설정해 주세요.',
'step' => 3,
]);
}
public function reset(Request $request)
{
$v = Validator::make($request->all(), [
'new_password' => ['required', 'string', 'min:8', 'max:72', 'confirmed'],
], [
'new_password.required' => '새 비밀번호를 입력해 주세요.',
'new_password.min' => '비밀번호는 8자 이상으로 입력해 주세요.',
'new_password.confirmed' => '비밀번호 확인이 일치하지 않습니다.',
]);
if ($v->fails()) {
return response()->json(['ok' => false, 'message' => $v->errors()->first()], 422);
}
$sess = $request->session()->get('find_pw');
if (!$sess || empty($sess['email']) || empty($sess['verified'])) {
return response()->json(['ok' => false, 'message' => '인증이 필요합니다.'], 403);
}
$email = (string) $sess['email'];
// (선택) 인증 후 너무 오래 지나면 재인증 요구
$verifiedAt = (int)($sess['verified_at'] ?? 0);
if ($verifiedAt > 0 && now()->timestamp - $verifiedAt > 10 * 60) { // 10분 예시
$request->session()->forget('find_pw');
return response()->json(['ok' => false, 'message' => '인증이 만료되었습니다. 다시 진행해 주세요.'], 403);
}
$newPassword = (string) $request->input('new_password');
// 실제 비밀번호 저장 컬럼은 프로젝트마다 다를 수 있어 안전하게 처리
$member = MemInfo::query()
->whereNotNull('email')
->where('email', '<>', '')
->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])
->orderByDesc('mem_no')
->first();
if (!$member) {
$request->session()->forget('find_pw');
return response()->json(['ok' => false, 'message' => '계정을 찾을 수 없습니다. 다시 진행해 주세요.'], 404);
}
// ✅ 여기서부터가 “진짜 저장 로직”
// MemInfo의 실제 컬럼명에 맞게 1개만 쓰면 됩니다.
// - password 컬럼을 쓰면 아래처럼
// - 레거시 passwd 컬럼이면 passwd로 교체
try {
if (isset($member->password)) {
$member->password = Hash::make($newPassword);
} elseif (isset($member->passwd)) {
$member->passwd = Hash::make($newPassword); // 레거시 규격이면 여기를 교체
} else {
// 컬럼을 모르면 여기서 명시적으로 막는게 안전
return response()->json([
'ok' => false,
'message' => '비밀번호 저장 컬럼 설정이 필요합니다. (MemInfo password/passwd 확인)',
], 500);
}
$member->save();
} catch (\Throwable $e) {
Log::error('FindPassword reset failed', [
'email' => $email,
'error' => $e->getMessage(),
]);
return response()->json(['ok' => false, 'message' => '비밀번호 변경 중 오류가 발생했습니다.'], 500);
}
$request->session()->forget('find_pw');
$request->session()->save();
return response()->json([
'ok' => true,
'message' => '비밀번호가 변경되었습니다. 로그인해 주세요.',
'redirect_url' => route('web.auth.login'),
]);
}
public function resetSession(Request $request)
{
$request->session()->forget('find_pw');
return response()->json(['ok' => true]);
}
}