middleware('guest')->except('logout'); を使用していた } /** * ログインフォームを表示 * * @return \Illuminate\View\View */ public function showLoginForm() { return view('auth.login'); } /** * ログインリクエストを処理 * Laravel 12変更点:AuthenticatesUsersトレイトを使わず独自実装 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function login(Request $request) { $this->validateLogin($request); // ログイン試行回数制限チェック if ($this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); return $this->sendLockoutResponse($request); } // ログイン認証試行 if ($this->attemptLogin($request)) { return $this->sendLoginResponse($request); } // ログイン失敗時の処理 $this->incrementLoginAttempts($request); return $this->sendFailedLoginResponse($request); } /** * ログインリクエストのバリデーション * * 仕様上の入力名(フォーム側)は ope_id / ope_pass のまま維持し、 * 内部の認証キーは login_id に寄せる(login_id 統一)。 * * @param \Illuminate\Http\Request $request * @return void */ protected function validateLogin(Request $request) { // 個別未入力メッセージ(仕様1,2) $request->validate([ 'ope_id' => 'required|string', // フォームの入力名は現状維持(実体は login_id) 'ope_pass' => 'required|string', ], [ 'ope_id.required' => 'ログインIDが未入力です。', 'ope_pass.required' => 'パスワードが未入力です。', ]); } /** * ログイン認証を試行 * * * - 画面入力(ope_id)= DBの login_id として扱う * - 退職フラグチェックも login_id で取得して判定する * * @param \Illuminate\Http\Request $request * @return bool */ protected function attemptLogin(Request $request) { // 先にIDのみでオペレータ取得して退職フラグを確認(仕様5-1) $loginId = $request->input('ope_id'); // 入力名は ope_id だが中身は login_id $operator = \App\Models\Ope::where('login_id', $loginId)->first(); if ($operator && (int)($operator->ope_quit_flag) === 1) { // 退職扱いは認証失敗と同じメッセージ(仕様5-1 と 3/4 統一表示) return false; } // 認証実行(credentials() で login_id / password を渡す) return Auth::attempt($this->credentials($request), false); } /** * 認証用の資格情報を取得 * * * - 認証IDを login_id に統一 * - パスワード入力(ope_pass)は Auth 側の password にマッピング * * @param \Illuminate\Http\Request $request * @return array */ protected function credentials(Request $request) { return [ 'login_id' => $request->input('ope_id'), // フォーム入力(ope_id)→ DB列 login_id 'password' => $request->input('ope_pass'), // フォーム入力(ope_pass)→ 認証用 password ]; } /** * ログイン成功時のレスポンス * * OTP認証チェック: * - 24時間以内に OTP 認証済みの場合:/home にリダイレクト * - 未認証の場合:OTP メール送信 → /otp にリダイレクト * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse */ protected function sendLoginResponse(Request $request) { $request->session()->regenerate(); $this->clearLoginAttempts($request); // 仕様5: ログインIDをセッション保持 // ここで保持する値も login_id(入力名は ope_id のまま) $request->session()->put('login_ope_id', $request->input('ope_id')); // OTP認証チェック $otpService = app(EmailOtpService::class); $user = Auth::user(); // 24時間以内に OTP 認証済みの場合 if ($otpService->isOtpRecent($user)) { return redirect()->intended($this->redirectTo); } // OTP 未認証の場合:OTP コード発行 → メール送信 → /otp にリダイレクト try { $otpCode = $otpService->issue($user); // ope_mail はセミコロン区切りで複数アドレスを保持する可能性があるため、最初のアドレスのみ抽出 $operatorEmails = explode(';', trim($user->ope_mail)); $primaryEmail = trim($operatorEmails[0] ?? $user->ope_mail); Log::info('OTP メール送信開始: ' . $primaryEmail); Mail::to($primaryEmail)->send(new EmailOtpMail( $otpCode, $user->name ?? 'ユーザー' )); Log::info('OTP メール送信完了: ' . $primaryEmail); return redirect()->route('otp.show') ->with('info', 'OTP認証コードをメール送信しました。'); } catch (\Exception $e) { Log::error('OTP issue/send failed: ' . $e->getMessage(), [ 'exception' => $e, 'user_id' => $user->ope_id ?? null, 'user_email' => $user->ope_mail ?? null, ]); // メール送信エラー時は home にリダイレクトするか、カスタムエラーを返す return redirect($this->redirectTo) ->with('warning', 'OTP認証メールの送信に失敗しました。'); } } /** * ログイン失敗時のレスポンス * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse */ protected function sendFailedLoginResponse(Request $request) { // 画面側のエラー表示キーは仕様に合わせて ope_id のまま throw ValidationException::withMessages([ 'ope_id' => [trans('auth.failed')], ]); } /** * ログアウト処理 * Laravel 12変更点:セッション無効化処理を追加 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse */ public function logout(Request $request) { Auth::logout(); // Laravel 12: セッション無効化とトークン再生成を明示的に実行 $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect('/login'); } /** * ログイン試行回数が上限を超えているかチェック * Laravel 12変更点:RateLimiterファサードを使用 * * @param \Illuminate\Http\Request $request * @return bool */ protected function hasTooManyLoginAttempts(Request $request) { return RateLimiter::tooManyAttempts( $this->throttleKey($request), 5 ); } /** * ログイン試行回数をインクリメント * * @param \Illuminate\Http\Request $request * @return void */ protected function incrementLoginAttempts(Request $request) { RateLimiter::hit( $this->throttleKey($request), 60 ); } /** * ログイン試行回数制限をクリア * * @param \Illuminate\Http\Request $request * @return void */ protected function clearLoginAttempts(Request $request) { RateLimiter::clear($this->throttleKey($request)); } /** * ロックアウト発生時のイベント処理 * * @param \Illuminate\Http\Request $request * @return void */ protected function fireLockoutEvent(Request $request) { // 必要に応じてイベントを発火 } /** * レート制限用のスロットルキーを取得 * * * - 画面入力名は ope_id のまま * - ただし内容は login_id を想定(ログインID文字列) * * @param \Illuminate\Http\Request $request * @return string */ protected function throttleKey(Request $request) { return Str::lower($request->input('ope_id')) . '|' . $request->ip(); } /** * ロックアウト時のレスポンス * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse */ protected function sendLockoutResponse(Request $request) { $seconds = RateLimiter::availableIn( $this->throttleKey($request) ); // 画面側のエラー表示キーは仕様に合わせて ope_id のまま throw ValidationException::withMessages([ 'ope_id' => [trans('auth.throttle', ['seconds' => $seconds])], ]); } }