262 lines
9.9 KiB
PHP
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]);
|
|
}
|
|
}
|