diff --git a/docs/stories/story-1.2-authentication-role-system.md b/docs/stories/story-1.2-authentication-role-system.md index 6ffc55f..42da0b9 100644 --- a/docs/stories/story-1.2-authentication-role-system.md +++ b/docs/stories/story-1.2-authentication-role-system.md @@ -1,91 +1,301 @@ # Story 1.2: Authentication & Role System +## Status +Draft + ## Epic Reference **Epic 1:** Core Foundation & Infrastructure -## User Story -As an **admin**, -I want **a secure authentication system with Admin/Client roles**, -So that **only authorized users can access the platform with appropriate permissions**. - -## Story Context - -### Existing System Integration -- **Integrates with:** Fortify authentication, users table -- **Technology:** Laravel Fortify, Livewire Volt -- **Follows pattern:** Existing `app/Actions/Fortify/` for custom logic -- **Touch points:** FortifyServiceProvider, login views, middleware +## Story +**As an** admin, +**I want** a secure authentication system with Admin/Client roles, +**So that** only authorized users can access the platform with appropriate permissions. ## Acceptance Criteria ### Functional Requirements -- [ ] Fortify configured with custom Volt views -- [ ] Login page with bilingual support (Arabic/English) -- [ ] Session timeout after 2 hours of inactivity -- [ ] Rate limiting on login attempts (5 attempts per minute) -- [ ] Admin role with full access to all features -- [ ] Client role with restricted access (own data only) -- [ ] Registration feature DISABLED (admin creates all accounts) +1. [ ] Fortify configured with custom Volt views +2. [ ] Login page with bilingual support (Arabic/English) +3. [ ] Session timeout after 2 hours of inactivity +4. [ ] Rate limiting on login attempts (5 attempts per minute) +5. [ ] Admin role with full access to all features +6. [ ] Client role with restricted access (own data only) +7. [ ] Registration feature DISABLED (admin creates all accounts) ### Security Requirements -- [ ] CSRF protection enabled on all forms -- [ ] Password hashing using bcrypt -- [ ] Gates/Policies for authorization checks -- [ ] Secure session configuration -- [ ] Remember me functionality (optional) +8. [ ] CSRF protection enabled on all forms +9. [ ] Password hashing using bcrypt +10. [ ] Custom middleware for admin authorization +11. [ ] Secure session configuration +12. [ ] Remember me functionality (optional) ### Integration Requirements -- [ ] Login redirects to appropriate dashboard: - - Admin users → `/admin/dashboard` (to be created in Epic 2) - - Client users → `/dashboard` (to be created in Epic 2) - - For now, redirect to a placeholder route or home page -- [ ] Logout clears session properly and redirects to login page -- [ ] Middleware protects admin-only routes (return 403 for unauthorized) -- [ ] Failed login attempts logged to `admin_logs` table +13. [ ] Login redirects to appropriate dashboard: + - Admin users → `/admin/dashboard` (placeholder until Epic 6) + - Client users → `/client/dashboard` (placeholder until Epic 7) +14. [ ] Logout clears session properly and redirects to login page +15. [ ] Admin middleware protects admin-only routes (return 403 for unauthorized) +16. [ ] Failed login attempts logged to `admin_logs` table +17. [ ] Deactivated users cannot log in ### Quality Requirements -- [ ] Login form validates inputs properly -- [ ] Error messages are clear and bilingual -- [ ] All test scenarios in "Test Scenarios" section pass -- [ ] No security vulnerabilities +18. [ ] Login form validates inputs properly +19. [ ] Error messages are clear and bilingual +20. [ ] All test scenarios in "Testing" section pass +21. [ ] No security vulnerabilities -## Technical Notes +## Tasks / Subtasks + +- [ ] **Task 1: Configure Fortify** (AC: 1, 7) + - [ ] Update `config/fortify.php` to disable registration feature + - [ ] Keep `resetPasswords` enabled (admin-triggered only via future Epic) + - [ ] Keep `emailVerification`, `updateProfileInformation`, `updatePasswords` enabled + - [ ] Keep `twoFactorAuthentication` enabled with confirm options + +- [ ] **Task 2: Add User Model Helper Methods** (AC: 5, 6) + - [ ] Add `isAdmin(): bool` method to `app/Models/User.php` + - [ ] Add `isClient(): bool` method to `app/Models/User.php` + - [ ] Add `isIndividual(): bool` method to `app/Models/User.php` + - [ ] Add `isCompany(): bool` method to `app/Models/User.php` + +- [ ] **Task 3: Create Custom Middleware** (AC: 10, 15, 17) + - [ ] Create `app/Http/Middleware/EnsureUserIsAdmin.php` + - [ ] Create `app/Http/Middleware/EnsureUserIsActive.php` + - [ ] Register middleware aliases in `bootstrap/app.php` as `admin` and `active` + - [ ] Add `SetLocale` middleware to web group (for bilingual support) + +- [ ] **Task 4: Create Login Volt Component** (AC: 1, 2, 8, 12, 18, 19) + - [ ] Create `resources/views/livewire/auth/login.blade.php` as class-based Volt component + - [ ] Implement form with email and password fields using Flux UI components + - [ ] Add remember me checkbox + - [ ] Add CSRF token via `@csrf` or Livewire handling + - [ ] Display validation errors in current locale + - [ ] Add language switcher or respect current locale + +- [ ] **Task 5: Configure Login Redirect Logic** (AC: 13, 14) + - [ ] Update `FortifyServiceProvider` to set custom login view + - [ ] Create `app/Actions/Fortify/RedirectIfAuthenticated.php` or configure in `AuthenticatedSessionController` + - [ ] Implement redirect logic: admin → `/admin/dashboard`, client → `/client/dashboard` + - [ ] Create placeholder routes for dashboards (return simple "Dashboard coming soon" view) + - [ ] Implement logout redirect to login page + +- [ ] **Task 6: Implement Session and Rate Limiting** (AC: 3, 4) + - [ ] Set `SESSION_LIFETIME=120` in `.env` (2 hours) + - [ ] Verify Fortify's built-in rate limiting (5 attempts per minute) + - [ ] Ensure rate limit error message uses translation key + +- [ ] **Task 7: Implement Login Attempt Logging** (AC: 16) + - [ ] Create listener for `Illuminate\Auth\Events\Failed` event + - [ ] Log failed attempts to `admin_logs` table with IP address + - [ ] Register listener in `EventServiceProvider` or `AppServiceProvider` + +- [ ] **Task 8: Implement Deactivated User Check** (AC: 17) + - [ ] Add check in `EnsureUserIsActive` middleware + - [ ] Or customize Fortify's `authenticateUsing` callback to reject deactivated users + - [ ] Return bilingual error message for deactivated accounts + +- [ ] **Task 9: Write Tests** (AC: 20) + - [ ] Create `tests/Feature/Auth/LoginTest.php` with all login flow tests + - [ ] Create `tests/Feature/Auth/AuthorizationTest.php` for middleware tests + - [ ] Create `tests/Unit/Models/UserHelperMethodsTest.php` for isAdmin/isClient tests + - [ ] Run all tests and ensure they pass + +- [ ] **Task 10: Final Verification** (AC: 21) + - [ ] Run `vendor/bin/pint` to format code + - [ ] Verify no security vulnerabilities (CSRF, session fixation, etc.) + - [ ] Test full login flow in browser for both admin and client users + +## Dev Notes + +### Relevant Source Tree +``` +app/ +├── Actions/ +│ └── Fortify/ # Existing - authentication logic +│ ├── CreateNewUser.php # Existing +│ ├── ResetUserPassword.php # Existing +│ └── UpdateUserPassword.php # Existing +├── Http/ +│ └── Middleware/ # Create these +│ ├── SetLocale.php +│ ├── EnsureUserIsAdmin.php +│ └── EnsureUserIsActive.php +├── Models/ +│ └── User.php # Existing - add helper methods +├── Providers/ +│ └── FortifyServiceProvider.php # Existing - configure views +bootstrap/ +└── app.php # Register middleware aliases +config/ +├── fortify.php # Configure features +└── session.php # Session configuration +resources/ +└── views/ + └── livewire/ + └── auth/ + └── login.blade.php # Create - Volt component +lang/ +├── ar/ +│ └── auth.php # Translation strings +└── en/ + └── auth.php # Translation strings +tests/ +├── Feature/ +│ └── Auth/ +│ ├── LoginTest.php +│ └── AuthorizationTest.php +└── Unit/ + └── Models/ + └── UserHelperMethodsTest.php +``` + +### User Model Helper Methods (Add to User.php) +```php +// app/Models/User.php +use App\Enums\UserType; +use App\Enums\UserStatus; + +public function isAdmin(): bool +{ + return $this->user_type === UserType::Admin; +} + +public function isClient(): bool +{ + return in_array($this->user_type, [UserType::Individual, UserType::Company]); +} + +public function isIndividual(): bool +{ + return $this->user_type === UserType::Individual; +} + +public function isCompany(): bool +{ + return $this->user_type === UserType::Company; +} + +public function isActive(): bool +{ + return $this->status === UserStatus::Active; +} +``` + +### Middleware Implementation (from Architecture Section 7.3) +```php +// app/Http/Middleware/EnsureUserIsAdmin.php +class EnsureUserIsAdmin +{ + public function handle(Request $request, Closure $next): Response + { + if (!$request->user()?->isAdmin()) { + abort(403, __('messages.unauthorized')); + } + + return $next($request); + } +} + +// app/Http/Middleware/EnsureUserIsActive.php +class EnsureUserIsActive +{ + public function handle(Request $request, Closure $next): Response + { + if ($request->user()?->status === UserStatus::Deactivated) { + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('login') + ->with('error', __('auth.account_deactivated')); + } + + return $next($request); + } +} +``` + +### Middleware Registration (bootstrap/app.php) +```php +// bootstrap/app.php +->withMiddleware(function (Middleware $middleware) { + $middleware->web(append: [ + \App\Http\Middleware\SetLocale::class, + ]); + + $middleware->alias([ + 'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class, + 'active' => \App\Http\Middleware\EnsureUserIsActive::class, + ]); +}) +``` ### Fortify Configuration ```php // config/fortify.php 'features' => [ - // Features::registration(), // DISABLED + // Features::registration(), // DISABLED - admin creates accounts Features::resetPasswords(), Features::emailVerification(), Features::updateProfileInformation(), Features::updatePasswords(), + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + ]), ], ``` -### Custom Views Setup +### FortifyServiceProvider Setup ```php -// FortifyServiceProvider boot() -Fortify::loginView(fn () => view('auth.login')); -// No registerView - registration disabled +// app/Providers/FortifyServiceProvider.php +public function boot(): void +{ + // Custom login view + Fortify::loginView(fn () => view('livewire.auth.login')); + + // Custom authentication logic (optional - for deactivated check) + Fortify::authenticateUsing(function (Request $request) { + $user = User::where('email', $request->email)->first(); + + if ($user && + Hash::check($request->password, $user->password) && + $user->status === UserStatus::Active) { + return $user; + } + + return null; + }); +} ``` -### Role Implementation -- Use `user_type` column: 'admin', 'individual', 'company' -- Admin check: `$user->user_type === 'admin'` -- Client check: `in_array($user->user_type, ['individual', 'company'])` - -### Gate Definitions +### Login Redirect Logic ```php -// AuthServiceProvider or AppServiceProvider -Gate::define('admin', fn (User $user) => $user->user_type === 'admin'); -Gate::define('client', fn (User $user) => $user->user_type !== 'admin'); +// app/Providers/FortifyServiceProvider.php or custom response +// After successful login, redirect based on role: +if ($user->isAdmin()) { + return redirect('/admin/dashboard'); +} +return redirect('/client/dashboard'); ``` -### Middleware -- `auth` - Require authentication -- `can:admin` - Require admin role -- Custom middleware for session timeout if needed +### Placeholder Dashboard Routes +```php +// routes/web.php +Route::middleware(['auth', 'active'])->group(function () { + // Admin routes + Route::middleware('admin')->prefix('admin')->group(function () { + Route::view('/dashboard', 'livewire.admin.dashboard-placeholder') + ->name('admin.dashboard'); + }); + + // Client routes + Route::prefix('client')->group(function () { + Route::view('/dashboard', 'livewire.client.dashboard-placeholder') + ->name('client.dashboard'); + }); +}); +``` ### Environment Configuration ```env @@ -93,56 +303,143 @@ SESSION_LIFETIME=120 # 2 hours in minutes SESSION_EXPIRE_ON_CLOSE=false ``` +### Translation Keys (lang/en/auth.php and lang/ar/auth.php) +```php +// lang/en/auth.php +return [ + 'failed' => 'These credentials do not match our records.', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + 'account_deactivated' => 'Your account has been deactivated. Please contact the administrator.', +]; + +// lang/ar/auth.php +return [ + 'failed' => 'بيانات الاعتماد هذه لا تتطابق مع سجلاتنا.', + 'password' => 'كلمة المرور المقدمة غير صحيحة.', + 'throttle' => 'محاولات تسجيل دخول كثيرة جداً. يرجى المحاولة مرة أخرى بعد :seconds ثانية.', + 'account_deactivated' => 'تم تعطيل حسابك. يرجى الاتصال بالمسؤول.', +]; +``` + ### Edge Case Behavior **Rate Limiting (5 attempts per minute):** -- After 5 failed attempts, show message: "Too many login attempts. Please try again in 60 seconds." -- Use bilingual message from translation files -- Log rate limit events for security monitoring +- Fortify handles this automatically via `RateLimiter` +- After 5 failed attempts, shows throttle message in current locale +- Lockout duration: 60 seconds **Session Timeout (2 hours inactivity):** +- Configured via `SESSION_LIFETIME=120` - When session expires, redirect to login page -- Show flash message: "Your session has expired. Please log in again." -- Preserve intended URL for redirect after re-authentication +- Intended URL preserved for redirect after re-authentication **Deactivated Account Login Attempt:** -- Check `status` field during authentication -- If `status === 'deactivated'`, reject login with message: "Your account has been deactivated. Please contact the administrator." +- Check in `Fortify::authenticateUsing()` callback +- Reject with `auth.account_deactivated` message +- User never gets authenticated -## Test Scenarios +### Flux UI Components for Login Form +```blade + + + +{{ __('Login') }} +``` -### Feature Tests (tests/Feature/Auth/) +## Testing + +### Test Location +- Feature tests: `tests/Feature/Auth/` +- Unit tests: `tests/Unit/Models/` + +### Testing Framework +Pest 4 with Volt testing support + +### Feature Tests (tests/Feature/Auth/LoginTest.php) **Login Flow:** -- [ ] `test_login_page_renders_correctly` - Login page displays in default language -- [ ] `test_user_can_login_with_valid_credentials` - Valid credentials redirect to dashboard -- [ ] `test_admin_redirects_to_admin_dashboard` - Admin user goes to admin dashboard -- [ ] `test_client_redirects_to_client_dashboard` - Client user goes to client dashboard -- [ ] `test_invalid_credentials_show_error` - Wrong password shows bilingual error -- [ ] `test_nonexistent_user_shows_error` - Unknown email shows generic error (no user enumeration) -- [ ] `test_deactivated_user_cannot_login` - Deactivated account rejected with message +- [ ] `test_login_page_renders_correctly` - Login page displays +- [ ] `test_user_can_login_with_valid_credentials` - Valid credentials authenticate +- [ ] `test_admin_redirects_to_admin_dashboard` - Admin → `/admin/dashboard` +- [ ] `test_client_redirects_to_client_dashboard` - Client → `/client/dashboard` +- [ ] `test_invalid_credentials_show_error` - Wrong password shows error +- [ ] `test_nonexistent_user_shows_error` - Unknown email shows generic error (no enumeration) +- [ ] `test_deactivated_user_cannot_login` - Deactivated account rejected **Rate Limiting:** - [ ] `test_rate_limiting_blocks_after_five_attempts` - 6th attempt blocked -- [ ] `test_rate_limit_message_is_bilingual` - Error message respects locale -- [ ] `test_rate_limit_resets_after_one_minute` - Can retry after cooldown +- [ ] `test_rate_limit_resets_after_cooldown` - Can retry after 60 seconds **Session Management:** -- [ ] `test_logout_clears_session` - Logout destroys session data -- [ ] `test_remember_me_extends_session` - Remember token works (if implemented) +- [ ] `test_logout_clears_session` - Logout destroys session +- [ ] `test_authenticated_user_cannot_access_login_page` - Redirect to dashboard -**Authorization:** -- [ ] `test_admin_can_access_admin_routes` - Admin passes `can:admin` middleware -- [ ] `test_client_cannot_access_admin_routes` - Client gets 403 on admin routes -- [ ] `test_unauthenticated_user_redirected_to_login` - Guest redirected from protected routes +### Feature Tests (tests/Feature/Auth/AuthorizationTest.php) -### Unit Tests (tests/Unit/) +**Middleware:** +- [ ] `test_admin_can_access_admin_routes` - Admin passes `admin` middleware +- [ ] `test_client_cannot_access_admin_routes` - Client gets 403 +- [ ] `test_unauthenticated_user_redirected_to_login` - Guest redirected +- [ ] `test_deactivated_user_logged_out_on_request` - Active middleware works -**Gate Definitions:** -- [ ] `test_admin_gate_returns_true_for_admin_user` - Gate check passes -- [ ] `test_admin_gate_returns_false_for_client_user` - Gate check fails -- [ ] `test_client_gate_returns_true_for_individual_user` - Client gate passes -- [ ] `test_client_gate_returns_true_for_company_user` - Client gate passes +### Unit Tests (tests/Unit/Models/UserHelperMethodsTest.php) + +**Helper Methods:** +- [ ] `test_isAdmin_returns_true_for_admin_user` +- [ ] `test_isAdmin_returns_false_for_client_user` +- [ ] `test_isClient_returns_true_for_individual_user` +- [ ] `test_isClient_returns_true_for_company_user` +- [ ] `test_isClient_returns_false_for_admin_user` +- [ ] `test_isActive_returns_true_for_active_user` +- [ ] `test_isActive_returns_false_for_deactivated_user` + +### Test Patterns +```php +// tests/Feature/Auth/LoginTest.php +use App\Models\User; +use App\Enums\UserType; +use App\Enums\UserStatus; +use Livewire\Volt\Volt; + +test('admin redirects to admin dashboard after login', function () { + $admin = User::factory()->create([ + 'user_type' => UserType::Admin, + 'status' => UserStatus::Active, + ]); + + $response = $this->post('/login', [ + 'email' => $admin->email, + 'password' => 'password', + ]); + + $response->assertRedirect('/admin/dashboard'); + $this->assertAuthenticatedAs($admin); +}); + +test('deactivated user cannot login', function () { + $user = User::factory()->create([ + 'status' => UserStatus::Deactivated, + ]); + + $response = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $this->assertGuest(); +}); + +test('client cannot access admin routes', function () { + $client = User::factory()->create([ + 'user_type' => UserType::Individual, + ]); + + $this->actingAs($client) + ->get('/admin/dashboard') + ->assertForbidden(); +}); +``` ## Definition of Done @@ -152,22 +449,33 @@ SESSION_EXPIRE_ON_CLOSE=false - [ ] Deactivated users cannot log in - [ ] Rate limiting prevents brute force (5 attempts/minute) - [ ] Session expires after 2 hours inactivity -- [ ] Admin routes protected from clients (403 response) -- [ ] All Feature and Unit tests from "Test Scenarios" section pass -- [ ] Code formatted with Pint +- [ ] Admin routes protected by `admin` middleware (403 for clients) +- [ ] Failed login attempts logged to `admin_logs` +- [ ] All Feature and Unit tests pass +- [ ] Code formatted with `vendor/bin/pint` ## Dependencies -- **Story 1.1:** Database schema - creates `users` table with `user_type` enum (`admin`, `individual`, `company`), `status` enum (`active`, `deactivated`), and `preferred_language` enum (`ar`, `en`) -- **Story 1.3:** Bilingual infrastructure (for login page translations) +- **Story 1.1:** Database schema - provides `users` table with `user_type` enum, `status` enum, and `preferred_language` field +- **Story 1.3:** Bilingual infrastructure - provides translation files and locale switching (can be developed in parallel) ## Risk Assessment -- **Primary Risk:** Security misconfiguration -- **Mitigation:** Use Laravel's built-in security features, no custom auth logic -- **Rollback:** Restore Fortify defaults +| Risk | Impact | Likelihood | Mitigation | +|------|--------|------------|------------| +| Security misconfiguration | High | Low | Use Laravel's built-in security features | +| Rate limiting bypass | Medium | Low | Use Fortify's built-in rate limiter | +| Session hijacking | High | Low | Use secure session configuration | +| Middleware registration issues | Low | Medium | Follow architecture Section 7.4 exactly | -## Estimation +### Rollback Plan +- Restore default Fortify configuration +- Remove custom middleware +- Revert User model changes -**Complexity:** Medium -**Estimated Effort:** 3-4 hours +## Change Log + +| Date | Version | Description | Author | +|------|---------|-------------|--------| +| Dec 21, 2025 | 1.0 | Initial story draft | SM Agent | +| Dec 21, 2025 | 2.0 | Complete rewrite: Added Status, Tasks/Subtasks, Dev Notes with Source Tree, fixed middleware naming (admin vs can:admin), fixed redirect paths (/client/dashboard), added User helper methods, aligned with architecture Section 7, added Change Log | Validation Task |