This commit is contained in:
parent
5dc60e0583
commit
6aa82dde3b
81
app/Console/Commands/CheckPasswordExpiry.php
Normal file
81
app/Console/Commands/CheckPasswordExpiry.php
Normal 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("");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
|||||||
135
app/Http/Controllers/Auth/PasswordChangeController.php
Normal file
135
app/Http/Controllers/Auth/PasswordChangeController.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
102
app/Http/Middleware/CheckPasswordChangeRequired.php
Normal file
102
app/Http/Middleware/CheckPasswordChangeRequired.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
97
app/Http/Requests/ChangePasswordRequest.php
Normal file
97
app/Http/Requests/ChangePasswordRequest.php
Normal 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' => '新パスワードと新パスワード確認は一致する必要があります。',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
159
resources/views/auth/password-change-success.blade.php
Normal file
159
resources/views/auth/password-change-success.blade.php
Normal 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>
|
||||||
259
resources/views/auth/password-change.blade.php
Normal file
259
resources/views/auth/password-change.blade.php
Normal 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>
|
||||||
@ -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 移行時の一時的な占位符路由
|
||||||
// 他の開発者が継続して開発できるように、エラーを防ぐための仮ルート定義
|
// 他の開発者が継続して開発できるように、エラーを防ぐための仮ルート定義
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user