【定期パスワード変更】実装
All checks were successful
Deploy main / deploy (push) Successful in 22s

This commit is contained in:
OU.ZAIKOU 2026-01-27 01:13:17 +09:00
parent 5dc60e0583
commit 6aa82dde3b
11 changed files with 885 additions and 3 deletions

View File

@ -0,0 +1,81 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Ope;
use Carbon\Carbon;
class CheckPasswordExpiry extends Command
{
/**
* コマンドの名前と説明
*
* @var string
*/
protected $signature = 'password:check-expiry {ope_id?}';
/**
* @var string
*/
protected $description = 'パスワード有効期限をチェック';
/**
* コマンド実行
*/
public function handle(): int
{
$opeId = $this->argument('ope_id');
if ($opeId) {
$ope = Ope::find($opeId);
if (!$ope) {
$this->error("Ope with ID {$opeId} not found");
return 1;
}
$this->checkOpe($ope);
} else {
// すべてのオペレータをチェック
$opes = Ope::all();
foreach ($opes as $ope) {
$this->checkOpe($ope);
}
}
return 0;
}
/**
* 単一オペレータの有効期限をチェック
*/
private function checkOpe(Ope $ope): void
{
$this->info("=== Ope ID: {$ope->ope_id} ({$ope->ope_name}) ===");
$this->info("ope_pass_changed_at: " . ($ope->ope_pass_changed_at ?? 'NULL'));
if (is_null($ope->ope_pass_changed_at)) {
$this->warn("❌ Password change REQUIRED (never changed)");
return;
}
try {
$changedAt = Carbon::parse($ope->ope_pass_changed_at);
$now = Carbon::now();
$monthsDiff = $now->diffInMonths($changedAt);
$this->info("Changed At: " . $changedAt->format('Y-m-d H:i:s'));
$this->info("Now: " . $now->format('Y-m-d H:i:s'));
$this->info("Months Difference: {$monthsDiff}");
if ($monthsDiff >= 3) {
$this->warn("❌ Password change REQUIRED ({$monthsDiff} months passed)");
} else {
$this->line("✅ Password is valid ({$monthsDiff} months passed, {3 - $monthsDiff} months remaining)");
}
} catch (\Exception $e) {
$this->error("Error parsing date: {$e->getMessage()}");
}
$this->line("");
}
}

View File

@ -59,7 +59,39 @@ class InformationController extends Controller
// ダッシュボード表示 // ダッシュボード表示
public function dashboard(Request $request) public function dashboard(Request $request)
{ {
return view('admin.information.dashboard'); // ダッシュボード統計情報を集計
// 駐輪場の総収容台数
$totalCapacity = DB::table('park')
->sum('park_capacity') ?? 0;
// 予約待ち人数regular_contractで状態チェック
$totalWaiting = DB::table('regular_contract')
->whereIn('rc_status', [1]) // 待機中など
->count();
// 利用率計算(使用中台数 / 総容量)
$utilizationRate = $totalCapacity > 0
? round((DB::table('park')
->where('park_status', 1)
->sum('park_capacity') ?? 0) / $totalCapacity * 100)
: 0;
$totalStats = [
'total_cities' => DB::table('city')->count(),
'total_parks' => DB::table('park')->count(),
'total_contracts' => DB::table('regular_contract')->count(),
'total_users' => DB::table('user')->count(),
'total_devices' => DB::table('device')->count(),
'today_queues' => DB::table('operator_que')
->whereDate('created_at', today())
->count(),
'total_waiting' => $totalWaiting,
'total_capacity' => $totalCapacity,
'total_utilization_rate' => $utilizationRate,
];
return view('admin.information.dashboard', compact('totalStats'));
} }
// ステータス一括更新(着手=2 / 対応完了=3 // ステータス一括更新(着手=2 / 対応完了=3

View File

@ -0,0 +1,135 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\ChangePasswordRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Validation\ValidationException;
use Carbon\Carbon;
class PasswordChangeController extends Controller
{
/**
* コントローラーのコンストラクタ
*
* ログイン状態のユーザーのみアクセス可能
*/
public function __construct()
{
// Laravel 12: ミドルウェアは routes/web.php で処理
}
/**
* パスワード変更フォーム表示
*
* GET /password/change
*
* @return \Illuminate\View\View
*/
public function showChangeForm()
{
// 現在のユーザー情報を取得
$ope = Auth::user();
// ビューにパスワード変更が必須かどうかを判定するデータを渡す
$isRequired = $this->isPasswordChangeRequired($ope);
return view('auth.password-change', [
'isRequired' => $isRequired,
]);
}
/**
* パスワード変更成功画面を表示
*
* GET /password/change/success
*
* @return \Illuminate\View\View
*/
public function showSuccessPage()
{
return view('auth.password-change-success');
}
/**
* パスワード変更処理
*
* POST /password/change
*
* バリデーション:
* - 当前パスワード必填、8-64文字、ハッシュ値一致確認
* - 新パスワード必填、8-64文字、英数字+記号のみ、当前と異なる
* - 新パスワード確認:必填、新パスワードと一致
*
* @param \App\Http\Requests\ChangePasswordRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function updatePassword(ChangePasswordRequest $request)
{
// 現在のユーザーを取得
$ope = Auth::user();
// ステップ1当前パスワードの認証ハッシュ値の確認
if (!Hash::check($request->current_password, $ope->ope_pass)) {
// バリデーションエラーとして当前パスワード が正しくないことを返す
throw ValidationException::withMessages([
'current_password' => '当前パスワードが正しくありません。',
]);
}
// ステップ2新パスワードが当前パスワードと同一でないか確認
// FormRequest側でも not_in ルールで確認しているが、ハッシュ値での二重チェック
if (Hash::check($request->password, $ope->ope_pass)) {
throw ValidationException::withMessages([
'password' => '新パスワードは当前パスワードと異なる必要があります。',
]);
}
// ステップ3データベース更新
// パスワードをハッシュ化して更新
$ope->ope_pass = Hash::make($request->password);
// パスワード変更時刻を現在時刻に更新
$ope->ope_pass_changed_at = Carbon::now();
// updated_at も自動更新される
$ope->save();
// イベント発火:パスワード変更イベント
event(new PasswordReset($ope));
// 成功画面へリダイレクト
return redirect()->route('password.change.success');
}
/**
* パスワード変更が必須かどうかを判定
*
* 初回ログイン時ope_pass_changed_at NULL)または
* 最後変更から3ヶ月以上経過している場合、TRUE を返す
*
* @param \App\Models\Ope $ope
* @return bool
*/
private function isPasswordChangeRequired($ope): bool
{
// パスワード変更日時が未設定(初回ログイン等)
if (is_null($ope->ope_pass_changed_at)) {
return true;
}
// パスワード変更から経過日数を計算
$changedAt = Carbon::parse($ope->ope_pass_changed_at);
$now = Carbon::now();
// 3ヶ月以上経過している場合
if ($now->diffInMonths($changedAt) >= 3) {
return true;
}
return false;
}
}

View File

@ -42,11 +42,14 @@ class ResetPasswordController extends Controller
} }
$user->password = Hash::make($request->password); $user->password = Hash::make($request->password);
$user->updated_at = now(); $user->updated_at = now();
// パスワード再設定時もope_pass_changed_atを更新
$user->ope_pass_changed_at = now();
$user->save(); $user->save();
// トークン削除 // トークン削除
DB::table('password_reset_tokens')->where('ope_mail', $request->email)->delete(); DB::table('password_reset_tokens')->where('ope_mail', $request->email)->delete();
return redirect()->route('login')->with('status', 'パスワードを再設定しました。'); // パスワード再設定成功画面へリダイレクト
return redirect()->route('password.change.success');
} }
} }

View File

@ -0,0 +1,102 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Symfony\Component\HttpFoundation\Response;
/**
* 定期パスワード変更チェックミドルウェア
*
* ログインしているオペレータのパスワード最後変更時刻をチェック
* 3ヶ月以上経過している場合、パスワード変更画面へ強制リダイレクト
*/
class CheckPasswordChangeRequired
{
/**
* リクエストを処理
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next): Response
{
// ログインしていない場合はスキップ
if (!Auth::check()) {
return $next($request);
}
// 既にパスワード変更ページにいる場合はスキップ
if ($request->routeIs('password.change.show', 'password.change.update')) {
return $next($request);
}
// 現在のユーザーを取得
$ope = Auth::user();
// パスワード変更が必須か判定
if ($this->isPasswordChangeRequired($ope)) {
return redirect()->route('password.change.show');
}
return $next($request);
}
/**
* パスワード変更が必須かどうかを判定
*
* 初回ログイン時ope_pass_changed_at NULL)または
* 最後変更から3ヶ月以上経過している場合、TRUE を返す
*
* @param \App\Models\Ope $ope
* @return bool
*/
private function isPasswordChangeRequired($ope): bool
{
// パスワード変更日時が未設定(初回ログイン等)
if (is_null($ope->ope_pass_changed_at)) {
\Log::info('Password change required: ope_pass_changed_at is null', [
'ope_id' => $ope->ope_id,
]);
return true;
}
// パスワード変更から経過日数を計算
// ope_pass_changed_at は複数のフォーマットに対応
try {
$changedAt = Carbon::parse($ope->ope_pass_changed_at);
} catch (\Exception $e) {
// パース失敗時は強制変更
\Log::warning('Failed to parse ope_pass_changed_at', [
'ope_id' => $ope->ope_id,
'value' => $ope->ope_pass_changed_at,
'error' => $e->getMessage(),
]);
return true;
}
$now = Carbon::now();
// 3ヶ月以上経過しているか判定
// diffInMonths は絶対値ではなく符号付きなので、abs() で絶対値を取得
$monthsDiff = abs($now->diffInMonths($changedAt));
\Log::info('Password change check', [
'ope_id' => $ope->ope_id,
'changed_at' => $changedAt->format('Y-m-d H:i:s'),
'now' => $now->format('Y-m-d H:i:s'),
'months_diff' => $monthsDiff,
'is_required' => $monthsDiff >= 3,
]);
if ($monthsDiff >= 3) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
class ChangePasswordRequest extends FormRequest
{
/**
* リクエストを処理することを認可するか判定
*
* @return bool
*/
public function authorize(): bool
{
return auth()->check();
}
/**
* 入力値の検証ルール
*
* クライアント側必填3項目、長度 8-64、新密码仅半角英数字+记号、新/确认一致
* サーバー側当前密码认证hash check、新密码不能等于旧密码CustomRulesで実装
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
// 当前パスワード必填、8-64文字
'current_password' => [
'required',
'string',
'min:8',
'max:64',
],
// 新パスワード必填、8-64文字、英数字+記号のみ
'password' => [
'required',
'string',
'min:8',
'max:64',
'regex:/^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:\'",.<>?\/\\|`~]+$/', // 半角英数字+記号のみ
],
// 新パスワード確認:必填、新パスワードと一致
'password_confirmation' => [
'required',
'string',
'same:password',
],
// hidden フィールド(フォーム側で出力)
'updated_at' => 'nullable|date_format:Y-m-d H:i:s',
'ope_pass_changed_at' => 'nullable|date_format:Y-m-d H:i:s',
];
}
/**
* 属性の表示名
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'current_password' => '当前パスワード',
'password' => '新パスワード',
'password_confirmation' => '新パスワード確認',
];
}
/**
* 検証エラーメッセージのカスタマイズ
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'current_password.required' => '当前パスワードを入力してください。',
'current_password.min' => '当前パスワードは8文字以上です。',
'current_password.max' => '当前パスワードは64文字以下です。',
'password.required' => '新パスワードを入力してください。',
'password.min' => '新パスワードは8文字以上です。',
'password.max' => '新パスワードは64文字以下です。',
'password.regex' => '新パスワードは英数字と記号のみ使用できます。',
'password_confirmation.required' => '新パスワード確認を入力してください。',
'password_confirmation.same' => '新パスワードと新パスワード確認は一致する必要があります。',
];
}
}

View File

@ -61,6 +61,8 @@ class Ope extends Authenticatable
'email_otp_expires_at', 'email_otp_expires_at',
'email_otp_last_sent_at', 'email_otp_last_sent_at',
'email_otp_verified_at', 'email_otp_verified_at',
// パスワード変更関連フィールド(定期パスワード変更)
'ope_pass_changed_at',
]; ];
/** /**

View File

@ -25,6 +25,7 @@ return Application::configure(basePath: dirname(__DIR__))
$middleware->alias([ $middleware->alias([
'check.city.access' => \App\Http\Middleware\CheckCityAccess::class, 'check.city.access' => \App\Http\Middleware\CheckCityAccess::class,
'ensure.otp.verified' => \App\Http\Middleware\EnsureOtpVerified::class, 'ensure.otp.verified' => \App\Http\Middleware\EnsureOtpVerified::class,
'check.password.change.required' => \App\Http\Middleware\CheckPasswordChangeRequired::class,
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {

View File

@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>パスワード変更完了 - So-Manager</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
<style>
body {
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.success-container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
max-width: 600px;
width: 100%;
padding: 40px;
text-align: center;
}
.success-icon {
width: 80px;
height: 80px;
margin: 0 auto 30px;
background-color: #28a745;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: white;
}
.success-title {
font-size: 28px;
font-weight: 700;
color: #333;
margin-bottom: 20px;
}
.success-message {
font-size: 16px;
color: #666;
line-height: 1.6;
margin-bottom: 30px;
}
.countdown {
font-size: 18px;
font-weight: 600;
color: #007bff;
margin-bottom: 30px;
}
.countdown-timer {
display: inline-block;
background-color: #e7f3ff;
border: 2px solid #007bff;
border-radius: 6px;
padding: 10px 20px;
min-width: 80px;
}
.return-login-btn {
padding: 14px 30px;
background-color: #f5f5f5;
border: 2px solid #333;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
color: #333;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.return-login-btn:hover {
background-color: #e8e8e8;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
color: #333;
text-decoration: none;
}
.return-login-btn:active {
transform: translateY(0);
}
.info-text {
font-size: 14px;
color: #999;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="success-container">
{{-- 成功アイコン --}}
<div class="success-icon"></div>
{{-- 成功タイトル --}}
<div class="success-title">パスワード変更完了</div>
{{-- 成功メッセージ --}}
<div class="success-message">
パスワードが正常に変更されました。<br>
システムは安全に保護されています。
</div>
{{-- カウントダウン --}}
<div class="countdown">
<span id="countdown-text">自動的にログイン画面に戻ります:</span><br>
<div class="countdown-timer">
<span id="countdown-timer">10</span>
</div>
</div>
{{-- 手動でログイン画面に戻るボタン --}}
<a href="{{ route('logout') }}" class="return-login-btn">
ログイン画面へ
</a>
{{-- 補足テキスト --}}
<div class="info-text">
自動的にログイン画面に遷移しない場合は、上記ボタンをクリックしてください。
</div>
</div>
<script>
// 10秒後に自動的にログアウトしてログイン画面へリダイレクト
let countdownSeconds = 10;
const countdownElement = document.getElementById('countdown-timer');
const countdownTextElement = document.getElementById('countdown-text');
const countdownInterval = setInterval(() => {
countdownSeconds--;
countdownElement.textContent = countdownSeconds;
if (countdownSeconds <= 0) {
clearInterval(countdownInterval);
// ログアウト処理を実行してからログイン画面へ遷移
window.location.href = "{{ route('logout') }}";
}
}, 1000);
</script>
</body>
</html>

View File

@ -0,0 +1,259 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>パスワード変更 - So-Manager</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
<style>
body {
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.password-change-container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
max-width: 600px;
width: 100%;
padding: 40px;
}
.panel-title {
text-align: center;
font-size: 28px;
font-weight: 700;
color: #333;
margin-bottom: 30px;
border-bottom: 3px solid #007bff;
padding-bottom: 15px;
}
.alert-warning-custom {
background-color: #fff3cd;
border: 2px solid #dc3545;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
color: #333;
line-height: 1.8;
}
.alert-warning-custom .text-danger {
color: #dc3545;
font-weight: 600;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
display: block;
font-size: 15px;
}
.form-group input[type="password"] {
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 15px;
transition: all 0.3s ease;
width: 100%;
}
.form-group input[type="password"]:focus {
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
outline: none;
}
.form-group input.is-invalid {
border-color: #dc3545;
}
.form-group input.is-invalid:focus {
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.25);
}
.invalid-feedback {
color: #dc3545;
font-size: 13px;
margin-top: 5px;
display: block;
}
.requirements {
background-color: #f8f9fa;
border-left: 4px solid #dc3545;
padding: 15px;
margin-bottom: 25px;
border-radius: 4px;
font-size: 14px;
color: #555;
}
.requirements ul {
margin: 0;
padding-left: 20px;
}
.requirements li {
margin-bottom: 8px;
}
.submit-btn {
width: 100%;
padding: 14px;
background-color: #f5f5f5;
border: 2px solid #333;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
color: #333;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.submit-btn:hover {
background-color: #e8e8e8;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.submit-btn:active {
transform: translateY(0);
}
.error-message {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 6px;
padding: 12px 15px;
margin-bottom: 20px;
color: #721c24;
}
.error-message ul {
margin: 0;
padding-left: 20px;
}
.error-message li {
margin-bottom: 5px;
}
.password-change-container {
max-width: 720px;
/* 或者 800 */
}
</style>
</head>
<body>
<div class="password-change-container">
{{-- パネルタイトル --}}
<div class="panel-title">So-Manager 管理パネル</div>
{{-- バリデーションエラー表示 --}}
@if ($errors->any())
<div class="error-message">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{-- 警告メッセージ --}}
<div class="alert-warning-custom">
<p>
セキュリティを維持するため、
<span class="text-danger">
パスワードの有効期限が切れました3ヶ月<br>
</span>
<span class="text-danger">
新しいパスワードへの変更をお願いいたします。<br>
</span>
安全にご利用いただくため、定期的なパスワード更新が必要です。<br>
変更後、引き続きシステムをご利用いただけます。
</p>
</div>
{{-- パスワード変更フォーム --}}
<form action="{{ route('password.change.update') }}" method="POST">
@csrf
{{-- 当前パスワード --}}
<div class="form-group">
<label for="current_password">現在のパスワード:</label>
<input type="password" class="@error('current_password') is-invalid @enderror" id="current_password"
name="current_password" required minlength="8" maxlength="64">
@error('current_password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
{{-- 新パスワード --}}
<div class="form-group">
<label for="password">新しいパスワード:</label>
<input type="password" class="@error('password') is-invalid @enderror" id="password" name="password"
required minlength="8" maxlength="64" pattern="[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:'\",.<>?/\\|`~]+">
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
{{-- 新パスワード確認 --}}
<div class="form-group">
<label for="password_confirmation">新しいパスワード(確認):</label>
<input type="password" class="@error('password_confirmation') is-invalid @enderror"
id="password_confirmation" name="password_confirmation" required minlength="8" maxlength="64"
pattern="[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:'\",.<>?/\\|`~]+">
@error('password_confirmation')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
{{-- 要件説明 --}}
<div class="requirements">
<strong>※新しいパスワードは、以下の条件を満たす必要があります。</strong>
<ul>
<li>8文字以上64文字以内</li>
<li>半角英数字および記号のみ使用可能</li>
<li>現在のパスワードと同一のものは使用できません</li>
</ul>
</div>
{{-- Hidden フィールド(設計書に記載) --}}
<input type="hidden" name="updated_at" value="{{ now()->format('Y-m-d H:i:s') }}">
<input type="hidden" name="ope_pass_changed_at" value="{{ now()->format('Y-m-d H:i:s') }}">
{{-- 送信ボタン & キャンセルボタン --}}
<div class="button-group">
<button type="submit" class="submit-btn">
パスワードを変更する
</button>
<a href="{{ route('logout') }}" class="cancel-btn">
ログイン画面へ
</a>
</div>
</form>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -90,10 +90,21 @@ Route::middleware('auth')->group(function () {
Route::post('/resend', [EmailOtpController::class, 'resend'])->name('resend'); Route::post('/resend', [EmailOtpController::class, 'resend'])->name('resend');
}); });
// パスワード変更ルートOTP チェック対象外、有効期限チェック対象)
// 定期的なパスワード変更が必須3ヶ月ごと
Route::prefix('password')->name('password.')->group(function () {
Route::get('/change', [App\Http\Controllers\Auth\PasswordChangeController::class, 'showChangeForm'])->name('change.show');
Route::post('/change', [App\Http\Controllers\Auth\PasswordChangeController::class, 'updatePassword'])->name('change.update');
Route::get('/success', [App\Http\Controllers\Auth\PasswordChangeController::class, 'showSuccessPage'])->name('change.success');
})->middleware('check.password.change.required');
// 以下のすべてのルートに OTP 認証チェックミドルウェアを適用 // 以下のすべてのルートに OTP 認証チェックミドルウェアを適用
Route::middleware('ensure.otp.verified')->group(function () { Route::middleware('ensure.otp.verified')->group(function () {
// ダッシュボード(ホーム画面) // ダッシュボード(ホーム画面)
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); // パスワード有効期限チェック対象3ヶ月超過時は強制リダイレクト
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])
->name('home')
->middleware('check.password.change.required');
// Laravel 12 移行時の一時的な占位符路由 // Laravel 12 移行時の一時的な占位符路由
// 他の開発者が継続して開発できるように、エラーを防ぐための仮ルート定義 // 他の開発者が継続して開発できるように、エラーを防ぐための仮ルート定義