315 lines
10 KiB
PHP
315 lines
10 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Auth;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Mail\EmailOtpMail;
|
||
use App\Services\EmailOtpService;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\Auth;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Illuminate\Support\Facades\Mail;
|
||
use Illuminate\Support\Facades\RateLimiter;
|
||
use Illuminate\Support\Str;
|
||
use Illuminate\Validation\ValidationException;
|
||
|
||
class LoginController extends Controller
|
||
{
|
||
/**
|
||
* ログイン成功後のリダイレクト先
|
||
*
|
||
* @var string
|
||
*/
|
||
protected $redirectTo = '/home';
|
||
|
||
/**
|
||
* コントローラーのインスタンス作成
|
||
* Laravel 12変更点:ミドルウェアは routes/web.php で処理するように変更
|
||
*
|
||
* @return void
|
||
*/
|
||
public function __construct()
|
||
{
|
||
// Laravel 12: ミドルウェアは routes/web.php で処理
|
||
// Laravel 5.7: $this->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])],
|
||
]);
|
||
}
|
||
}
|