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]); } }