diff --git a/app/Actions/User/EditUserAction.php b/app/Actions/User/EditUserAction.php new file mode 100644 index 0000000..ccb931a --- /dev/null +++ b/app/Actions/User/EditUserAction.php @@ -0,0 +1,63 @@ + $user->id, + 'old_name' => $user->name, + 'new_name' => $data['name'], + 'planner_id' => $user->planner_id, + ]); + + $result = $user->update([ + 'name' => $data['name'], + ]); + + Log::info('EditUserAction: Update result', [ + 'result' => $result, + 'user_id' => $user->id, + ]); + + if (!$result) { + throw new \Exception('User update returned false'); + } + + // Verify the update actually happened + $user->refresh(); + if ($user->name !== $data['name']) { + throw new \Exception('User update did not persist to database'); + } + + DB::commit(); + + Log::info('EditUserAction: User successfully updated', [ + 'user_id' => $user->id, + 'updated_name' => $user->name, + ]); + + return true; + + } catch (\Exception $e) { + DB::rollBack(); + + Log::error('EditUserAction: User update failed', [ + 'user_id' => $user->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + throw $e; + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..80375a9 --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,44 @@ +validate([ + 'email' => ['required', 'email'], + 'password' => ['required'], + ]); + + if (Auth::attempt($credentials, $request->boolean('remember'))) { + $request->session()->regenerate(); + + return redirect()->intended(route('dashboard')); + } + + throw ValidationException::withMessages([ + 'email' => 'These credentials do not match our records.', + ]); + } + + public function logout(Request $request) + { + Auth::logout(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect('/'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php new file mode 100644 index 0000000..8a4c546 --- /dev/null +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -0,0 +1,37 @@ +validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:planners'], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + $user = Planner::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + ]); + + Auth::login($user); + + return redirect(route('dashboard')); + } +} \ No newline at end of file diff --git a/app/Livewire/Auth/Login.php b/app/Livewire/Auth/Login.php deleted file mode 100644 index 91b50e0..0000000 --- a/app/Livewire/Auth/Login.php +++ /dev/null @@ -1,36 +0,0 @@ -validate(); - - if (Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) { - session()->regenerate(); - return redirect()->intended(route('dashboard')); - } - - $this->addError('email', 'These credentials do not match our records.'); - } - - public function render() - { - return view('livewire.auth.login') - ->layout('components.layouts.guest'); - } -} \ No newline at end of file diff --git a/app/Livewire/Auth/Register.php b/app/Livewire/Auth/Register.php deleted file mode 100644 index eb74e0a..0000000 --- a/app/Livewire/Auth/Register.php +++ /dev/null @@ -1,45 +0,0 @@ -validate(); - - $planner = Planner::create([ - 'name' => $this->name, - 'email' => $this->email, - 'password' => Hash::make($this->password), - ]); - - Auth::login($planner); - session()->regenerate(); - - return redirect()->route('dashboard'); - } - - public function render() - { - return view('livewire.auth.register') - ->layout('components.layouts.guest'); - } -} \ No newline at end of file diff --git a/app/Livewire/Dishes/DishesList.php b/app/Livewire/Dishes/DishesList.php index 7b947bc..392ff2a 100644 --- a/app/Livewire/Dishes/DishesList.php +++ b/app/Livewire/Dishes/DishesList.php @@ -33,7 +33,7 @@ public function render() ->orderBy('name') ->paginate(10); - $users = User::where('planner_id', auth()->user()->planner_id) + $users = User::where('planner_id', auth()->id()) ->orderBy('name') ->get(); @@ -56,7 +56,7 @@ public function store() $dish = Dish::create([ 'name' => $this->name, - 'planner_id' => auth()->user()->planner_id, + 'planner_id' => auth()->id(), ]); // Attach selected users diff --git a/app/Livewire/Schedule/ScheduleGenerator.php b/app/Livewire/Schedule/ScheduleGenerator.php index 9843bdf..59d692d 100644 --- a/app/Livewire/Schedule/ScheduleGenerator.php +++ b/app/Livewire/Schedule/ScheduleGenerator.php @@ -24,14 +24,14 @@ public function mount() $this->selectedYear = now()->year; // Select all users by default - $this->selectedUsers = User::where('planner_id', auth()->user()->planner_id) + $this->selectedUsers = User::where('planner_id', auth()->id()) ->pluck('id') ->toArray(); } public function render() { - $users = User::where('planner_id', auth()->user()->planner_id) + $users = User::where('planner_id', auth()->id()) ->orderBy('name') ->get(); @@ -108,7 +108,7 @@ public function generate() 'user_id' => $userId, 'dish_id' => $randomDish['id'], 'date' => $currentDate->format('Y-m-d'), - 'planner_id' => auth()->user()->planner_id, + 'planner_id' => auth()->id(), ]); } } @@ -152,7 +152,7 @@ public function regenerateForDate($date) 'user_id' => $userId, 'dish_id' => $randomDish->id, 'date' => $currentDate->format('Y-m-d'), - 'planner_id' => auth()->user()->planner_id, + 'planner_id' => auth()->id(), ]); } } diff --git a/app/Livewire/Users/UsersList.php b/app/Livewire/Users/UsersList.php index 963be2c..da1d196 100644 --- a/app/Livewire/Users/UsersList.php +++ b/app/Livewire/Users/UsersList.php @@ -3,6 +3,9 @@ namespace App\Livewire\Users; use App\Models\User; +use App\Actions\User\EditUserAction; +use Exception; +use Illuminate\Contracts\View\View; use Livewire\Component; use Livewire\WithPagination; @@ -10,127 +13,100 @@ class UsersList extends Component { use WithPagination; - public $showCreateModal = false; - public $showEditModal = false; - public $showDeleteModal = false; - - public $editingUser = null; - public $deletingUser = null; - + public bool $showCreateModal = false; + public bool $showEditModal = false; + public bool $showDeleteModal = false; + + public ?User $editingUser = null; + public ?User $deletingUser = null; + // Form fields - public $name = ''; - public $email = ''; - public $password = ''; - public $password_confirmation = ''; - - protected $rules = [ + public string $name = ''; + + protected array $rules = [ 'name' => 'required|string|max:255', - 'email' => 'required|email|unique:users,email', - 'password' => 'required|min:8|confirmed', ]; - public function render() + public function render(): View { - $users = User::where('planner_id', auth()->user()->planner_id) + $users = User::where('planner_id', auth()->id()) ->orderBy('name') ->paginate(10); - + return view('livewire.users.users-list', [ 'users' => $users ]); } - public function create() + public function create(): void { - $this->reset(['name', 'email', 'password', 'password_confirmation']); + $this->reset(['name']); $this->resetValidation(); $this->showCreateModal = true; } - public function store() + public function store(): void { $this->validate(); User::create([ 'name' => $this->name, - 'email' => $this->email, - 'password' => bcrypt($this->password), - 'planner_id' => auth()->user()->planner_id, + 'planner_id' => auth()->id(), ]); $this->showCreateModal = false; - $this->reset(['name', 'email', 'password', 'password_confirmation']); - + $this->reset(['name']); + session()->flash('success', 'User created successfully.'); } - public function edit(User $user) + public function edit(User $user): void { $this->editingUser = $user; $this->name = $user->name; - $this->email = $user->email; - $this->password = ''; - $this->password_confirmation = ''; $this->resetValidation(); $this->showEditModal = true; } - public function update() + public function update(): void { - $rules = [ - 'name' => 'required|string|max:255', - 'email' => 'required|email|unique:users,email,' . $this->editingUser->id, - ]; - - if ($this->password) { - $rules['password'] = 'min:8|confirmed'; - } - - $this->validate($rules); + $this->validate(); - $this->editingUser->update([ - 'name' => $this->name, - 'email' => $this->email, - ]); - - if ($this->password) { - $this->editingUser->update([ - 'password' => bcrypt($this->password) - ]); - } + try { + (new EditUserAction())->execute($this->editingUser, ['name' => $this->name]); - $this->showEditModal = false; - $this->reset(['name', 'email', 'password', 'password_confirmation', 'editingUser']); - - session()->flash('success', 'User updated successfully.'); + $this->showEditModal = false; + $this->reset(['name', 'editingUser']); + + session()->flash('success', 'User updated successfully.'); + + // Force component to re-render with fresh data + $this->resetPage(); + } catch (Exception $e) { + session()->flash('error', 'Failed to update user: ' . $e->getMessage()); + } } - public function confirmDelete(User $user) + public function confirmDelete(User $user): void { $this->deletingUser = $user; $this->showDeleteModal = true; } - public function delete() + public function delete(): void { - if ($this->deletingUser->id === auth()->id()) { - session()->flash('error', 'You cannot delete your own account.'); - $this->showDeleteModal = false; - return; - } - $this->deletingUser->delete(); $this->showDeleteModal = false; $this->deletingUser = null; - + session()->flash('success', 'User deleted successfully.'); } - public function cancel() + public function cancel(): void { $this->showCreateModal = false; $this->showEditModal = false; $this->showDeleteModal = false; - $this->reset(['name', 'email', 'password', 'password_confirmation', 'editingUser', 'deletingUser']); + $this->reset(['name', 'editingUser', 'deletingUser']); } -} \ No newline at end of file +} diff --git a/app/Models/User.php b/app/Models/User.php index 9675e87..6639c90 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,8 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; -use Illuminate\Foundation\Auth\User as Authenticatable; -use Illuminate\Notifications\Notifiable; +use Illuminate\Database\Eloquent\Model; /** * @property int $id @@ -20,22 +19,17 @@ * @property Collection $userDishes * @method static User findOrFail(int $user_id) * @method static UserFactory factory($count = null, $state = []) + * @method static create(array $array) + * @method static where(string $string, int|string|null $id) */ -class User extends Authenticatable +class User extends Model { /** @use HasFactory */ - use HasFactory, Notifiable; + use HasFactory; protected $fillable = [ 'planner_id', 'name', - 'email', - 'password', - ]; - - protected $hidden = [ - 'password', - 'remember_token', ]; protected static function booted(): void @@ -43,19 +37,6 @@ protected static function booted(): void static::addGlobalScope(new BelongsToPlanner); } - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } - public function dishes(): BelongsToMany { return $this->belongsToMany(Dish::class, 'user_dishes', 'user_id', 'dish_id'); diff --git a/bootstrap/app.php b/bootstrap/app.php index 5c511ee..c6a8eab 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -9,9 +9,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; -use Illuminate\Http\Middleware\HandleCors; use Illuminate\Http\Request; -use Illuminate\Session\Middleware\StartSession; use Illuminate\Validation\ValidationException; use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -27,8 +25,6 @@ ->withMiddleware(function (Middleware $middleware) { // Apply ForceJsonResponse only to API routes $middleware->api(ForceJsonResponse::class); - $middleware->append(StartSession::class); - $middleware->append(HandleCors::class); }) ->withExceptions(function (Exceptions $exceptions) { $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { @@ -42,19 +38,32 @@ /** @var OutputService $outputService */ $outputService = resolve(OutputService::class); - $exceptions->render(fn (ValidationException $e, Request $request) => $outputService - ->response(false, null, [$e->getMessage()], 404) - ); + $exceptions->render(function (ValidationException $e, Request $request) use ($outputService) { + if ($request->is('api/*') || $request->expectsJson()) { + return response()->json( + $outputService->response(false, null, [$e->getMessage()]), + 404 + ); + } + }); - $exceptions->render(fn (NotFoundHttpException $e, Request $request) => response()->json( - $outputService->response(false, null, ['MODEL_NOT_FOUND']), - 404 - )); + $exceptions->render(function (NotFoundHttpException $e, Request $request) use ($outputService) { + if ($request->is('api/*') || $request->expectsJson()) { + return response()->json( + $outputService->response(false, null, ['MODEL_NOT_FOUND']), + 404 + ); + } + }); - $exceptions->render(fn (AccessDeniedHttpException $e, Request $request) => response()->json( - $outputService->response(false, null, [$e->getMessage()]), - 403 - )); + $exceptions->render(function (AccessDeniedHttpException $e, Request $request) use ($outputService) { + if ($request->is('api/*') || $request->expectsJson()) { + return response()->json( + $outputService->response(false, null, [$e->getMessage()]), + 403 + ); + } + }); }) ->withCommands([ GenerateScheduleCommand::class, diff --git a/phpunit.dusk.xml b/phpunit.dusk.xml new file mode 100644 index 0000000..24fbe45 --- /dev/null +++ b/phpunit.dusk.xml @@ -0,0 +1,15 @@ + + + + + ./tests/Browser + + + diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/auth/login.blade.php similarity index 59% rename from resources/views/livewire/auth/login.blade.php rename to resources/views/auth/login.blade.php index 373ad72..35a637d 100644 --- a/resources/views/livewire/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -1,32 +1,43 @@ -
+@extends('components.layouts.guest') + +@section('content')

Login

-
+ + @csrf +
- - @error('email') {{ $message }} @enderror + @error('email') + {{ $message }} + @enderror
- - @error('password') {{ $message }} @enderror + name="password" + class="w-full p-2 mb-4 border rounded bg-gray-600 border-secondary text-gray-100 focus:bg-gray-900 focus:outline-none focus:border-accent-blue @error('password') border-red-500 @enderror" + placeholder="Enter your password" + required> + @error('password') + {{ $message }} + @enderror
@@ -42,4 +53,4 @@ class="rounded border-secondary bg-gray-600 text-primary focus:ring-accent-blue"
-
\ No newline at end of file +@endsection \ No newline at end of file diff --git a/resources/views/livewire/auth/register.blade.php b/resources/views/auth/register.blade.php similarity index 54% rename from resources/views/livewire/auth/register.blade.php rename to resources/views/auth/register.blade.php index 28f444f..70293f9 100644 --- a/resources/views/livewire/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -1,46 +1,61 @@ -
+@extends('components.layouts.guest') + +@section('content')

Register

-
+ + @csrf +
- - @error('name') {{ $message }} @enderror + @error('name') + {{ $message }} + @enderror
- +
- - @error('email') {{ $message }} @enderror + name="email" + value="{{ old('email') }}" + class="w-full p-2 mb-4 border rounded bg-gray-600 border-secondary text-gray-100 focus:bg-gray-900 focus:outline-none focus:border-accent-blue @error('email') border-red-500 @enderror" + placeholder="Enter your email" + required> + @error('email') + {{ $message }} + @enderror
-
- - @error('password') {{ $message }} @enderror + name="password" + class="w-full p-2 mb-4 border rounded bg-gray-600 border-secondary text-gray-100 focus:bg-gray-900 focus:outline-none focus:border-accent-blue @error('password') border-red-500 @enderror" + placeholder="Enter your password" + required> + @error('password') + {{ $message }} + @enderror
- + placeholder="Confirm your password" + required>
- \ No newline at end of file +@endsection \ No newline at end of file diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php index d5d472b..7757a8b 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/components/layouts/app.blade.php @@ -101,6 +101,58 @@ class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-gray- @livewireScripts + + {{-- CSRF Token Auto-Refresh for Livewire --}} + + diff --git a/resources/views/components/layouts/guest.blade.php b/resources/views/components/layouts/guest.blade.php index 592a07f..ca4cdd8 100644 --- a/resources/views/components/layouts/guest.blade.php +++ b/resources/views/components/layouts/guest.blade.php @@ -19,11 +19,62 @@
- {{ $slot }} + @yield('content')
@livewireScripts + + {{-- CSRF Token Auto-Refresh for Livewire --}} + \ No newline at end of file diff --git a/resources/views/livewire/users/users-list.blade.php b/resources/views/livewire/users/users-list.blade.php index 8d09395..81c593d 100644 --- a/resources/views/livewire/users/users-list.blade.php +++ b/resources/views/livewire/users/users-list.blade.php @@ -30,21 +30,20 @@ class="py-2 px-4 bg-primary text-white text-xl rounded hover:bg-secondary transi

{{ $user->name }}

-

{{ $user->email }}

- @if($user->id !== auth()->id()) - - @endif +
@empty @@ -66,7 +65,7 @@ class="px-3 py-1 bg-danger text-white rounded hover:bg-red-700 transition-colors

Add New User

-
+
{{ $message }} @enderror
-
- - - @error('email') {{ $message }} @enderror -
- -
- - - @error('password') {{ $message }} @enderror -
- -
- - -
-