Add password change page for user profiles

- New ProfileController with editPassword/updatePassword actions that verify
  the current password and require a confirmed new password of at least 8 chars
- Adds /profile/password GET/PUT routes behind auth middleware
- Adds profile.password blade view with form validation error display
- Links "Change Password" in the app layout sidebar
This commit is contained in:
Brian Fertig 2026-09-21 11:03:03 -06:00
parent 7eb2751cd4
commit 17da8d9c3c
4 changed files with 79 additions and 0 deletions

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Password;
use Illuminate\View\View;
class ProfileController extends Controller
{
public function editPassword(): View
{
return view('profile.password');
}
public function updatePassword(Request $request): RedirectResponse
{
$data = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', 'string', 'confirmed', Password::min(8)],
]);
$request->user()->update(['password' => $data['password']]);
return redirect()
->route('profile.password.edit')
->with('status', 'Your password has been updated.');
}
}

View File

@ -61,6 +61,7 @@
<a href="{{ route('teams.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Teams</a> <a href="{{ route('teams.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Teams</a>
<a href="{{ route('categories.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Categories</a> <a href="{{ route('categories.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Categories</a>
<a href="{{ route('api-keys.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">API Keys</a> <a href="{{ route('api-keys.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">API Keys</a>
<a href="{{ route('profile.password.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Change Password</a>
</div> </div>
</div> </div>

View File

@ -0,0 +1,43 @@
@extends('layouts.app')
@section('title', 'Change Password — ' . config('app.name'))
@section('content')
<div class="mx-auto max-w-md rounded-lg border border-gray-200 bg-white p-8">
<h1 class="mb-2 text-2xl font-bold">Change password</h1>
<p class="mb-6 text-gray-600">Update the password for your account.</p>
<form method="POST" action="{{ route('profile.password.update') }}">
@csrf
@method('PUT')
<div class="mb-4">
<label for="current_password" class="mb-1 block font-semibold">Current password</label>
<input id="current_password" type="password" name="current_password" required autofocus autocomplete="current-password"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('current_password')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror
</div>
<div class="mb-4">
<label for="password" class="mb-1 block font-semibold">New password</label>
<input id="password" type="password" name="password" required autocomplete="new-password"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('password')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror
</div>
<div class="mb-6">
<label for="password_confirmation" class="mb-1 block font-semibold">Confirm new password</label>
<input id="password_confirmation" type="password" name="password_confirmation" required autocomplete="new-password"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
</div>
<div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Update password</button>
</div>
</form>
</div>
@endsection

View File

@ -7,6 +7,7 @@ use App\Http\Controllers\CategoryController;
use App\Http\Controllers\GroupAdminController; use App\Http\Controllers\GroupAdminController;
use App\Http\Controllers\KpiController; use App\Http\Controllers\KpiController;
use App\Http\Controllers\KpiEntryController; use App\Http\Controllers\KpiEntryController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ProjectController; use App\Http\Controllers\ProjectController;
use App\Http\Controllers\ProjectEntryController; use App\Http\Controllers\ProjectEntryController;
use App\Http\Controllers\TeamController; use App\Http\Controllers\TeamController;
@ -81,6 +82,10 @@ Route::middleware('auth')->group(function () {
Route::put('/categories/{category}', [CategoryController::class, 'update'])->name('categories.update'); Route::put('/categories/{category}', [CategoryController::class, 'update'])->name('categories.update');
Route::delete('/categories/{category}', [CategoryController::class, 'destroy'])->name('categories.destroy'); Route::delete('/categories/{category}', [CategoryController::class, 'destroy'])->name('categories.destroy');
// Profile
Route::get('/profile/password', [ProfileController::class, 'editPassword'])->name('profile.password.edit');
Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password.update');
// API keys // API keys
Route::get('/api-keys', [ApiKeyController::class, 'index'])->name('api-keys.index'); Route::get('/api-keys', [ApiKeyController::class, 'index'])->name('api-keys.index');
Route::post('/api-keys', [ApiKeyController::class, 'store'])->name('api-keys.store'); Route::post('/api-keys', [ApiKeyController::class, 'store'])->name('api-keys.store');